Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(25)

Side by Side Diff: chrome/browser/resources/extensions/extension_list.js

Issue 893453002: Stop rebuilding all elements in chrome://extensions to preserve focus on refresh (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Merged to single file Created 5 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 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 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 <include src="extension_error.js"> 5 <include src="extension_error.js">
6 6
7 /** 7 /**
8 * The type of the extension data object. The definition is based on 8 * The type of the extension data object. The definition is based on
9 * chrome/browser/ui/webui/extensions/extension_basic_info.cc 9 * chrome/browser/ui/webui/extensions/extension_basic_info.cc
10 * and 10 * and
(...skipping 59 matching lines...) Expand 10 before | Expand all | Expand 10 after
70 70
71 /** 71 /**
72 * Creates a new list of extensions. 72 * Creates a new list of extensions.
73 * @param {Object=} opt_propertyBag Optional properties. 73 * @param {Object=} opt_propertyBag Optional properties.
74 * @constructor 74 * @constructor
75 * @extends {HTMLDivElement} 75 * @extends {HTMLDivElement}
76 */ 76 */
77 var ExtensionsList = cr.ui.define('div'); 77 var ExtensionsList = cr.ui.define('div');
78 78
79 /** 79 /**
80 * @type {Object.<string, boolean>} A map from extension id to a boolean
81 * indicating whether the incognito warning is showing. This persists
82 * between calls to decorate.
83 */
84 var butterBarVisibility = {};
85
86 /**
87 * @type {Object.<string, number>} A map from extension id to last reloaded 80 * @type {Object.<string, number>} A map from extension id to last reloaded
88 * timestamp. The timestamp is recorded when the user click the 'Reload' 81 * timestamp. The timestamp is recorded when the user click the 'Reload'
89 * link. It is used to refresh the icon of an unpacked extension. 82 * link. It is used to refresh the icon of an unpacked extension.
90 * This persists between calls to decorate. 83 * This persists between calls to decorate.
91 */ 84 */
92 var extensionReloadedTimestamp = {}; 85 var extensionReloadedTimestamp = {};
93 86
94 ExtensionsList.prototype = { 87 ExtensionsList.prototype = {
95 __proto__: HTMLDivElement.prototype, 88 __proto__: HTMLDivElement.prototype,
96 89
97 /** 90 /**
98 * Indicates whether an embedded options page that was navigated to through 91 * Indicates whether an embedded options page that was navigated to through
99 * the '?options=' URL query has been shown to the user. This is necessary 92 * the '?options=' URL query has been shown to the user. This is necessary
100 * to prevent showExtensionNodes_ from opening the options more than once. 93 * to prevent showExtensionNodes_ from opening the options more than once.
101 * @type {boolean} 94 * @type {boolean}
102 * @private 95 * @private
103 */ 96 */
104 optionsShown_: false, 97 optionsShown_: false,
105 98
99 /**
100 * Necessary to only show the butterbar once.
101 * @private {boolean}
102 */
103 butterbarShown_: false,
104
106 decorate: function() { 105 decorate: function() {
107 this.textContent = '';
108
109 this.showExtensionNodes_(); 106 this.showExtensionNodes_();
110 }, 107 },
111 108
112 getIdQueryParam_: function() { 109 getIdQueryParam_: function() {
113 return parseQueryParams(document.location)['id']; 110 return parseQueryParams(document.location)['id'];
114 }, 111 },
115 112
116 getOptionsQueryParam_: function() { 113 getOptionsQueryParam_: function() {
117 return parseQueryParams(document.location)['options']; 114 return parseQueryParams(document.location)['options'];
118 }, 115 },
119 116
120 /** 117 /**
121 * Creates all extension items from scratch. 118 * Creates all extension items from scratch.
122 * @private 119 * @private
123 */ 120 */
124 showExtensionNodes_: function() { 121 showExtensionNodes_: function() {
122 // Any node that is not updated will be deleted.
123 var nodes = document.querySelectorAll('.extension-list-item-wrapper');
124 for (var i = 0; i < nodes.length; ++i) {
125 nodes[i].classList.add('to-remove');
not at google - send to devlin 2015/02/11 19:25:35 It seems odd to be modifying a node's class to mar
hcarmona 2015/02/11 23:36:47 Moving to the JS wrapper simplifies the code so we
not at google - send to devlin 2015/02/12 01:04:50 Alright. Well, I prefer the version which doesn't
Dan Beam 2015/02/12 01:08:32 what if we just make a superset of all the IDs in
hcarmona 2015/02/12 02:33:12 Since we're only ever adding or removing 1 extensi
not at google - send to devlin 2015/02/12 18:34:39 What do you mean by a "class"? A DOM class (like y
Dan Beam 2015/02/12 20:46:19 ^ ping
126 }
127
125 // Iterate over the extension data and add each item to the list. 128 // Iterate over the extension data and add each item to the list.
126 this.data_.extensions.forEach(this.createNode_, this); 129 this.data_.extensions.forEach(function(extension) {
130 var node = $(extension.id);
131
132 if (node)
133 this.updateNode_(extension, node);
134 else
135 this.createNode_(extension);
136 }, this);
137
138 // Delete any node that was not updated
139 var toRemove = document.querySelectorAll(
140 '.extension-list-item-wrapper.to-remove');
Dan Beam 2015/02/11 20:09:43 .extension-list-item-wrapper.to-remove[id]
hcarmona 2015/02/11 23:36:46 Done.
141 for (var i = 0; i < toRemove.length; ++i) {
142 var node = toRemove[i];
143 if (node.id && node.parentElement)
Dan Beam 2015/02/11 20:09:43 document.qSA() doesn't work without a parentElemen
hcarmona 2015/02/11 23:36:46 Done.
144 node.parentElement.removeChild(node);
not at google - send to devlin 2015/02/11 19:25:35 I think this is a bug. A NodeList (which is what q
not at google - send to devlin 2015/02/11 19:29:30 Ok, apparenetly querySelectorAll returns a "static
hcarmona 2015/02/11 23:36:47 Acknowledged.
145 }
127 146
128 var idToHighlight = this.getIdQueryParam_(); 147 var idToHighlight = this.getIdQueryParam_();
129 if (idToHighlight && $(idToHighlight)) 148 if (idToHighlight && $(idToHighlight))
130 this.scrollToNode_(idToHighlight); 149 this.scrollToNode_(idToHighlight);
131 150
132 var idToOpenOptions = this.getOptionsQueryParam_(); 151 var idToOpenOptions = this.getOptionsQueryParam_();
133 if (idToOpenOptions && $(idToOpenOptions)) 152 if (idToOpenOptions && $(idToOpenOptions))
134 this.showEmbeddedExtensionOptions_(idToOpenOptions, true); 153 this.showEmbeddedExtensionOptions_(idToOpenOptions, true);
135 154
136 var noExtensions = this.data_.extensions.length == 0; 155 var noExtensions = this.data_.extensions.length == 0;
(...skipping 21 matching lines...) Expand all
158 * given in |extension|. 177 * given in |extension|.
159 * @param {ExtensionData} extension A dictionary of extension metadata. 178 * @param {ExtensionData} extension A dictionary of extension metadata.
160 * @private 179 * @private
161 */ 180 */
162 createNode_: function(extension) { 181 createNode_: function(extension) {
163 var template = $('template-collection').querySelector( 182 var template = $('template-collection').querySelector(
164 '.extension-list-item-wrapper'); 183 '.extension-list-item-wrapper');
165 var node = template.cloneNode(true); 184 var node = template.cloneNode(true);
166 node.id = extension.id; 185 node.id = extension.id;
167 186
168 if (!extension.enabled || extension.terminated) 187 // The 'Show Browser Action' button.
169 node.classList.add('inactive-extension'); 188 this.addListener_('click', node, '.show-button', function(e) {
189 chrome.send('extensionSettingsShowButton', [extension.id]);
190 });
170 191
192 // The 'allow in incognito' checkbox.
193 this.addListener_('change', node, '.incognito-control input',
194 function(e) {
195 var butterBar = node.querySelector('.butter-bar');
196 var checked = e.target.checked;
197 if (!this.butterbarShown_) {
198 butterBar.hidden = !checked || extension.is_hosted_app;
199 this.butterbarShown_ = !butterBar.hidden;
200 } else
201 butterBar.hidden = true;
202 chrome.send('extensionSettingsEnableIncognito',
203 [extension.id, String(checked)]);
204 }.bind(this));
205
206 // The 'collect errors' checkbox. This should only be visible if the
207 // error console is enabled - we can detect this by the existence of the
208 // |errorCollectionEnabled| property.
209 this.addListener_('change', node, '.error-collection-control input',
210 function(e) {
211 chrome.send('extensionSettingsEnableErrorCollection',
212 [extension.id, String(e.target.checked)]);
213 });
214
215 // The 'allow on all urls' checkbox. This should only be visible if
216 // active script restrictions are enabled. If they are not enabled, no
217 // extensions should want all urls.
218 this.addListener_('click', node, '.all-urls-control', function(e) {
219 chrome.send('extensionSettingsAllowOnAllUrls',
220 [extension.id, String(e.target.checked)]);
221 });
222
223 // The 'allow file:// access' checkbox.
224 this.addListener_('click', node, '.file-access-control', function(e) {
225 chrome.send('extensionSettingsAllowFileAccess',
226 [extension.id, String(e.target.checked)]);
227 });
228
229 // The 'Options' button or link, depending on its behaviour.
230 // Set an href to get the correct mouse-over appearance (link,
231 // footer) - but the actual link opening is done through chrome.send
232 // with a preventDefault().
233 node.querySelector('.options-link').setAttribute(
Dan Beam 2015/02/11 20:09:43 nit: `.href =` is shorter
hcarmona 2015/02/11 23:36:46 Done.
234 'href', extension.optionsPageHref);
235 this.addListener_('click', node, '.options-link', function(e) {
236 chrome.send('extensionSettingsOptions', [extension.id]);
237 e.preventDefault();
238 });
239
240 this.addListener_('click', node, '.options-button', function(e) {
241 this.showEmbeddedExtensionOptions_(extension.id, false);
242 e.preventDefault();
243 }.bind(this));
244
245 // The 'Permissions' link.
246 this.addListener_('click', node, '.permissions-link', function(e) {
247 chrome.send('extensionSettingsPermissions', [extension.id]);
248 e.preventDefault();
249 });
250
251 // The 'Reload' link.
252 this.addListener_('click', node, '.reload-link', function(e) {
253 chrome.send('extensionSettingsReload', [extension.id]);
254 extensionReloadedTimestamp[extension.id] = Date.now();
255 });
256
257 // The 'Launch' link.
258 this.addListener_('click', node, '.launch-link', function(e) {
259 chrome.send('extensionSettingsLaunch', [extension.id]);
260 });
261
262 // The 'Reload' terminated link.
263 this.addListener_('click', node, '.terminated-reload-link', function(e) {
264 chrome.send('extensionSettingsReload', [extension.id]);
265 });
266
267 // The 'Repair' corrupted link.
268 this.addListener_('click', node, '.corrupted-repair-button', function(e) {
269 chrome.send('extensionSettingsRepair', [extension.id]);
270 });
271
272 // The 'Enabled' checkbox.
273 this.addListener_('click', node, '.enable-checkbox input', function(e) {
Dan Beam 2015/02/11 20:09:43 why click and not 'change'?
hcarmona 2015/02/11 23:36:47 Done.
274 var checked = e.target.checked;
275 chrome.send('extensionSettingsEnable', [extension.id, String(checked)]);
276
277 // This may seem counter-intuitive (to not set/clear the checkmark)
278 // but this page will be updated asynchronously if the extension
279 // becomes enabled/disabled. It also might not become enabled or
280 // disabled, because the user might e.g. get prompted when enabling
281 // and choose not to.
282 e.preventDefault();
283 });
284
285 // 'Remove' button.
286 var trashTemplate = $('template-collection').querySelector('.trash');
287 var trash = trashTemplate.cloneNode(true);
288 trash.title = loadTimeData.getString('extensionUninstall');
289 trash.addEventListener('click', function(e) {
290 chrome.send('extensionSettingsUninstall', [extension.id]);
291 });
292 node.querySelector('.enable-controls').appendChild(trash);
293
294 // Developer mode ////////////////////////////////////////////////////////
295
296 // The path, if provided by unpacked extension.
297 this.addListener_('click', node, '.load-path a:nth-of-type(1)',
Dan Beam 2015/02/11 20:09:43 :first-of-type
hcarmona 2015/02/11 23:36:47 Done.
298 function(e) {
299 chrome.send('extensionSettingsShowPath', [String(extension.id)]);
300 e.preventDefault();
301 });
302
303 this.appendChild(node);
304 this.updateNode_(extension, node);
305 },
306
307 /**
308 * Updates an HTML element for the extension metadata given in |extension|.
309 * @param {ExtensionData} extension A dictionary of extension metadata.
310 * @param {Element} node The node that is being updated.
311 * @private
312 */
313 updateNode_: function(extension, node) {
314 node.classList.remove('to-remove');
315 node.classList.toggle('inactive-extension',
316 !extension.enabled || extension.terminated);
317
318 var toRemove = ['policy-controlled', 'may-not-modify', 'may-not-remove'];
319 node.classList.remove.apply(node.classList, toRemove);
Dan Beam 2015/02/11 20:09:43 node.classList.remove('policy-controlled', 'may-no
hcarmona 2015/02/11 23:36:47 Done.
171 var classes = []; 320 var classes = [];
172 if (extension.managedInstall) { 321 if (extension.managedInstall) {
173 classes.push('policy-controlled', 'may-not-modify'); 322 classes.push('policy-controlled', 'may-not-modify');
174 } else if (extension.dependentExtensions.length > 0) { 323 } else if (extension.dependentExtensions.length > 0) {
175 classes.push('may-not-remove', 'may-not-modify'); 324 classes.push('may-not-remove', 'may-not-modify');
176 } else if (extension.recommendedInstall) { 325 } else if (extension.recommendedInstall) {
177 classes.push('may-not-remove'); 326 classes.push('may-not-remove');
178 } else if (extension.suspiciousInstall || 327 } else if (extension.suspiciousInstall ||
179 extension.corruptInstall || 328 extension.corruptInstall ||
180 extension.updateRequiredByPolicy) { 329 extension.updateRequiredByPolicy) {
181 classes.push('may-not-modify'); 330 classes.push('may-not-modify');
182 } 331 }
183 node.classList.add.apply(node.classList, classes); 332 node.classList.add.apply(node.classList, classes);
184 333
185 var idToHighlight = this.getIdQueryParam_(); 334 node.classList.toggle('extension-highlight',
186 if (node.id == idToHighlight) 335 node.id == this.getIdQueryParam_());
187 node.classList.add('extension-highlight');
188 336
189 var item = node.querySelector('.extension-list-item'); 337 var item = node.querySelector('.extension-list-item');
190 // Prevent the image cache of extension icon by using the reloaded 338 // Prevent the image cache of extension icon by using the reloaded
191 // timestamp as a query string. The timestamp is recorded when the user 339 // timestamp as a query string. The timestamp is recorded when the user
192 // clicks the 'Reload' link. http://crbug.com/159302. 340 // clicks the 'Reload' link. http://crbug.com/159302.
193 if (extensionReloadedTimestamp[extension.id]) { 341 if (extensionReloadedTimestamp[extension.id]) {
194 item.style.backgroundImage = 342 item.style.backgroundImage =
195 'url(' + extension.icon + '?' + 343 'url(' + extension.icon + '?' +
196 extensionReloadedTimestamp[extension.id] + ')'; 344 extensionReloadedTimestamp[extension.id] + ')';
197 } else { 345 } else {
198 item.style.backgroundImage = 'url(' + extension.icon + ')'; 346 item.style.backgroundImage = 'url(' + extension.icon + ')';
199 } 347 }
200 348
201 var title = node.querySelector('.extension-title'); 349 this.setTextContent_(node, '.extension-title', extension.name);
202 title.textContent = extension.name; 350 this.setTextContent_(node, '.extension-version', extension.version);
203 351 this.setTextContent_(node, '.location-text', extension.locationText);
204 var version = node.querySelector('.extension-version'); 352 this.setTextContent_(node, '.blacklist-text', extension.blacklistText);
205 version.textContent = extension.version; 353 this.setTextContent_(node, '.extension-description',
206 354 extension.description);
207 var locationText = node.querySelector('.location-text');
208 locationText.textContent = extension.locationText;
209
210 var blacklistText = node.querySelector('.blacklist-text');
211 blacklistText.textContent = extension.blacklistText;
212
213 var description = document.createElement('span');
214 description.textContent = extension.description;
215 node.querySelector('.extension-description').appendChild(description);
216 355
217 // The 'Show Browser Action' button. 356 // The 'Show Browser Action' button.
218 if (extension.enable_show_button) { 357 this.updateVisibility_(node, '.show-button',
219 var showButton = node.querySelector('.show-button'); 358 extension.enable_show_button);
220 showButton.addEventListener('click', function(e) {
221 chrome.send('extensionSettingsShowButton', [extension.id]);
222 });
223 showButton.hidden = false;
224 }
225 359
226 // The 'allow in incognito' checkbox. 360 // The 'allow in incognito' checkbox.
227 node.querySelector('.incognito-control').hidden = 361 this.updateVisibility_(node, '.incognito-control',
228 !this.data_.incognitoAvailable; 362 this.data_.incognitoAvailable, function(item) {
229 var incognito = node.querySelector('.incognito-control input'); 363 var incognito = item.querySelector('input');
230 incognito.disabled = !extension.incognitoCanBeEnabled; 364 incognito.disabled = !extension.incognitoCanBeEnabled;
231 incognito.checked = extension.enabledIncognito; 365 incognito.checked = extension.enabledIncognito;
232 if (!incognito.disabled) { 366 });
233 incognito.addEventListener('change', function(e) { 367
234 var checked = e.target.checked; 368 // Hide butterBar if incognito is not enabled for the extension.
235 butterBarVisibility[extension.id] = checked;
236 butterBar.hidden = !checked || extension.is_hosted_app;
237 chrome.send('extensionSettingsEnableIncognito',
238 [extension.id, String(checked)]);
239 });
240 }
241 var butterBar = node.querySelector('.butter-bar'); 369 var butterBar = node.querySelector('.butter-bar');
242 butterBar.hidden = !butterBarVisibility[extension.id]; 370 butterBar.hidden = butterBar.hidden || !extension.enabledIncognito;
243 371
244 // The 'collect errors' checkbox. This should only be visible if the 372 // The 'collect errors' checkbox. This should only be visible if the
245 // error console is enabled - we can detect this by the existence of the 373 // error console is enabled - we can detect this by the existence of the
246 // |errorCollectionEnabled| property. 374 // |errorCollectionEnabled| property.
247 if (extension.wantsErrorCollection) { 375 this.updateVisibility_(node, '.error-collection-control',
248 node.querySelector('.error-collection-control').hidden = false; 376 extension.wantsErrorCollection, function(item) {
249 var errorCollection = 377 item.querySelector('input').checked = extension.errorCollectionEnabled;
250 node.querySelector('.error-collection-control input'); 378 });
251 errorCollection.checked = extension.errorCollectionEnabled;
252 errorCollection.addEventListener('change', function(e) {
253 chrome.send('extensionSettingsEnableErrorCollection',
254 [extension.id, String(e.target.checked)]);
255 });
256 }
257 379
258 // The 'allow on all urls' checkbox. This should only be visible if 380 // The 'allow on all urls' checkbox. This should only be visible if
259 // active script restrictions are enabled. If they are not enabled, no 381 // active script restrictions are enabled. If they are not enabled, no
260 // extensions should want all urls. 382 // extensions should want all urls.
261 if (extension.showAllUrls) { 383 this.updateVisibility_(node, '.all-urls-control', extension.showAllUrls,
262 var allUrls = node.querySelector('.all-urls-control'); 384 function(item) {
263 allUrls.addEventListener('click', function(e) { 385 item.querySelector('input').checked = extension.allowAllUrls;
264 chrome.send('extensionSettingsAllowOnAllUrls', 386 });
265 [extension.id, String(e.target.checked)]);
266 });
267 allUrls.querySelector('input').checked = extension.allowAllUrls;
268 allUrls.hidden = false;
269 }
270 387
271 // The 'allow file:// access' checkbox. 388 // The 'allow file:// access' checkbox.
272 if (extension.wantsFileAccess) { 389 this.updateVisibility_(node, '.file-access-control',
273 var fileAccess = node.querySelector('.file-access-control'); 390 extension.wantsFileAccess, function(item) {
274 fileAccess.addEventListener('click', function(e) { 391 item.querySelector('input').checked = extension.allowFileAccess;
275 chrome.send('extensionSettingsAllowFileAccess', 392 });
276 [extension.id, String(e.target.checked)]);
277 });
278 fileAccess.querySelector('input').checked = extension.allowFileAccess;
279 fileAccess.hidden = false;
280 }
281 393
282 // The 'Options' button or link, depending on its behaviour. 394 // The 'Options' button or link, depending on its behaviour.
283 if (extension.enabled && extension.optionsUrl) { 395 var optionsEnabled = extension.enabled && extension.optionsUrl;
284 var options, optionsClickListener; 396 this.updateVisibility_(node, '.options-link', optionsEnabled &&
285 if (extension.optionsOpenInTab) { 397 extension.optionsOpenInTab);
286 options = node.querySelector('.options-link'); 398 this.updateVisibility_(node, '.options-button', optionsEnabled &&
287 // Set an href to get the correct mouse-over appearance (link, 399 !extension.optionsOpenInTab);
288 // footer) - but the actual link opening is done through chrome.send
289 // with a preventDefault().
290 options.setAttribute('href', extension.optionsPageHref);
291 optionsClickListener = function() {
292 chrome.send('extensionSettingsOptions', [extension.id]);
293 };
294 } else {
295 options = node.querySelector('.options-button');
296 optionsClickListener = function() {
297 this.showEmbeddedExtensionOptions_(extension.id, false);
298 }.bind(this);
299 }
300 options.addEventListener('click', function(e) {
301 optionsClickListener();
302 e.preventDefault();
303 });
304 options.hidden = false;
305 }
306 400
307 // The 'Permissions' link. 401 // The 'View in Web Store/View Web Site' link.
308 var permissions = node.querySelector('.permissions-link'); 402 var siteLinkEnabled = extension.homepageUrl &&
309 permissions.addEventListener('click', function(e) { 403 !extension.enableExtensionInfoDialog;
310 chrome.send('extensionSettingsPermissions', [extension.id]); 404 this.updateVisibility_(node, '.site-link', siteLinkEnabled,
311 e.preventDefault(); 405 function(item) {
406 item.href = extension.homepageUrl;
407 item.textContent = loadTimeData.getString(
408 extension.homepageProvided ? 'extensionSettingsVisitWebsite' :
409 'extensionSettingsVisitWebStore');
312 }); 410 });
313 411
314 // The 'View in Web Store/View Web Site' link. 412 // The 'Reload' link.
315 if (extension.homepageUrl && !extension.enableExtensionInfoDialog) { 413 this.updateVisibility_(node, '.reload-link', extension.allow_reload);
316 var siteLink = node.querySelector('.site-link');
317 siteLink.href = extension.homepageUrl;
318 siteLink.textContent = loadTimeData.getString(
319 extension.homepageProvided ? 'extensionSettingsVisitWebsite' :
320 'extensionSettingsVisitWebStore');
321 siteLink.hidden = false;
322 }
323 414
324 if (extension.allow_reload) { 415 // The 'Launch' link.
325 // The 'Reload' link. 416 this.updateVisibility_(node, '.launch-link', extension.allow_reload &&
326 var reload = node.querySelector('.reload-link'); 417 extension.is_platform_app);
327 reload.addEventListener('click', function(e) {
328 chrome.send('extensionSettingsReload', [extension.id]);
329 extensionReloadedTimestamp[extension.id] = Date.now();
330 });
331 reload.hidden = false;
332 418
333 if (extension.is_platform_app) { 419 // The 'Reload' terminated link.
334 // The 'Launch' link. 420 var isTerminated = extension.terminated;
335 var launch = node.querySelector('.launch-link'); 421 this.updateVisibility_(node, '.terminated-reload-link', isTerminated);
336 launch.addEventListener('click', function(e) {
337 chrome.send('extensionSettingsLaunch', [extension.id]);
338 });
339 launch.hidden = false;
340 }
341 }
342 422
343 if (extension.terminated) { 423 // The 'Repair' corrupted link.
344 var terminatedReload = node.querySelector('.terminated-reload-link'); 424 var canRepair = !isTerminated && extension.corruptInstall &&
345 terminatedReload.hidden = false; 425 extension.isFromStore;
346 terminatedReload.onclick = function() { 426 this.updateVisibility_(node, '.corrupted-repair-button', canRepair);
347 chrome.send('extensionSettingsReload', [extension.id]); 427
348 }; 428 // The 'Enabled' checkbox.
349 } else if (extension.corruptInstall && extension.isFromStore) { 429 var isOK = !isTerminated && !canRepair;
350 var repair = node.querySelector('.corrupted-repair-button'); 430 this.updateVisibility_(node, '.enable-checkbox', isOK, function(item) {
351 repair.hidden = false;
352 repair.onclick = function() {
353 chrome.send('extensionSettingsRepair', [extension.id]);
354 };
355 } else {
356 // The 'Enabled' checkbox.
357 var enable = node.querySelector('.enable-checkbox');
358 enable.hidden = false;
359 var enableCheckboxDisabled = extension.managedInstall || 431 var enableCheckboxDisabled = extension.managedInstall ||
360 extension.suspiciousInstall || 432 extension.suspiciousInstall ||
361 extension.corruptInstall || 433 extension.corruptInstall ||
362 extension.updateRequiredByPolicy || 434 extension.updateRequiredByPolicy ||
363 extension.dependentExtensions.length > 0; 435 extension.dependentExtensions.length > 0;
364 enable.querySelector('input').disabled = enableCheckboxDisabled; 436 item.querySelector('input').disabled = enableCheckboxDisabled;
437 item.querySelector('input').checked = extension.enabled;
438 });
365 439
366 if (extension.managedInstall) { 440 // Button for extensions controlled by policy.
367 var indicator = new cr.ui.ControlledIndicator(); 441 var controlNode = node.querySelector('.enable-controls');
368 indicator.classList.add('controlled-extension-indicator'); 442 var indicator =
369 indicator.setAttribute('controlled-by', 'policy'); 443 controlNode.querySelector('.controlled-extension-indicator');
370 var textPolicy = extension.policyText || ''; 444 var needsIndicator = isOK && extension.managedInstall;
371 indicator.setAttribute('textpolicy', textPolicy);
372 indicator.image.setAttribute('aria-label', textPolicy);
373 node.querySelector('.enable-controls').appendChild(indicator);
374 }
375 445
376 if (!enableCheckboxDisabled) { 446 if (needsIndicator && !indicator) {
377 enable.addEventListener('click', function(e) { 447 indicator = new cr.ui.ControlledIndicator();
378 // When e.target is the label instead of the checkbox, it doesn't 448 indicator.classList.add('controlled-extension-indicator');
379 // have the checked property and the state of the checkbox is 449 indicator.setAttribute('controlled-by', 'policy');
380 // left unchanged. 450 var textPolicy = extension.policyText || '';
381 var checked = e.target.checked; 451 indicator.setAttribute('textpolicy', textPolicy);
382 if (checked == undefined) 452 indicator.image.setAttribute('aria-label', textPolicy);
383 checked = !e.currentTarget.querySelector('input').checked; 453 controlNode.appendChild(indicator);
384 chrome.send('extensionSettingsEnable', 454 } else if (!needsIndicator && indicator)
385 [extension.id, checked ? 'true' : 'false']); 455 controlNode.removeChild(indicator);
386
387 // This may seem counter-intuitive (to not set/clear the checkmark)
388 // but this page will be updated asynchronously if the extension
389 // becomes enabled/disabled. It also might not become enabled or
390 // disabled, because the user might e.g. get prompted when enabling
391 // and choose not to.
392 e.preventDefault();
393 });
394 }
395
396 enable.querySelector('input').checked = extension.enabled;
397 }
398
399 // 'Remove' button.
400 var trashTemplate = $('template-collection').querySelector('.trash');
401 var trash = trashTemplate.cloneNode(true);
402 trash.title = loadTimeData.getString('extensionUninstall');
403 trash.addEventListener('click', function(e) {
404 butterBarVisibility[extension.id] = false;
405 chrome.send('extensionSettingsUninstall', [extension.id]);
406 });
407 node.querySelector('.enable-controls').appendChild(trash);
408 456
409 // Developer mode //////////////////////////////////////////////////////// 457 // Developer mode ////////////////////////////////////////////////////////
410 458
411 // First we have the id. 459 // First we have the id.
412 var idLabel = node.querySelector('.extension-id'); 460 var idLabel = node.querySelector('.extension-id');
413 idLabel.textContent = ' ' + extension.id; 461 idLabel.textContent = ' ' + extension.id;
414 462
415 // Then the path, if provided by unpacked extension. 463 // Then the path, if provided by unpacked extension.
416 if (extension.isUnpacked) { 464 this.updateVisibility_(node, '.load-path', extension.isUnpacked,
417 var loadPath = node.querySelector('.load-path'); 465 function(item) {
418 loadPath.hidden = false; 466 item.querySelector('a:nth-of-type(1)').textContent =
Dan Beam 2015/02/11 20:09:43 :first-of-type
hcarmona 2015/02/11 23:36:46 Done.
419 var pathLink = loadPath.querySelector('a:nth-of-type(1)'); 467 ' ' + extension.prettifiedPath;
420 pathLink.textContent = ' ' + extension.prettifiedPath; 468 });
421 pathLink.addEventListener('click', function(e) {
422 chrome.send('extensionSettingsShowPath', [String(extension.id)]);
423 e.preventDefault();
424 });
425 }
426 469
427 // Then the 'managed, cannot uninstall/disable' message. 470 // Then the 'managed, cannot uninstall/disable' message.
428 if (extension.managedInstall || extension.recommendedInstall) { 471 // We would like to hide managed installed message since this
429 node.querySelector('.managed-message').hidden = false; 472 // extension is disabled.
430 } else { 473 var isRequired = extension.managedInstall || extension.recommendedInstall;
431 if (extension.suspiciousInstall) { 474 this.updateVisibility_(node, '.managed-message', isRequired &&
432 // Then the 'This isn't from the webstore, looks suspicious' message. 475 !extension.updateRequiredByPolicy);
433 node.querySelector('.suspicious-install-message').hidden = false; 476
434 } 477 // Then the 'This isn't from the webstore, looks suspicious' message.
435 if (extension.corruptInstall) { 478 this.updateVisibility_(node, '.suspicious-install-message', !isRequired &&
436 // Then the 'This is a corrupt extension' message. 479 extension.suspiciousInstall);
437 node.querySelector('.corrupt-install-message').hidden = false; 480
438 } 481 // Then the 'This is a corrupt extension' message.
439 } 482 this.updateVisibility_(node, '.corrupt-install-message', !isRequired &&
483 extension.corruptInstall);
440 484
441 // Then the 'An update required by enterprise policy' message. Note that 485 // Then the 'An update required by enterprise policy' message. Note that
442 // a force-installed extension might be disabled due to being outdated 486 // a force-installed extension might be disabled due to being outdated
443 // as well. 487 // as well.
444 if (extension.updateRequiredByPolicy) { 488 this.updateVisibility_(node, '.update-required-message',
445 node.querySelector('.update-required-message').hidden = false; 489 extension.updateRequiredByPolicy);
446 // We would like to hide managed installed message since this
447 // extension is disabled.
448 node.querySelector('.managed-message').hidden = true;
449 }
450 490
451 if (extension.dependentExtensions.length > 0) { 491 // The 'following extensions depend on this extension' list.
452 node.classList.add('developer-extras'); 492 var hasDependents = extension.dependentExtensions.length > 0;
453 var dependentMessage = 493 node.classList.toggle('developer-extras', hasDependents);
454 node.querySelector('.dependent-extensions-message'); 494 this.updateVisibility_(node, '.dependent-extensions-message',
455 dependentMessage.hidden = false; 495 hasDependents, function(item) {
456 var dependentList = dependentMessage.querySelector('ul'); 496 var dependentList = item.querySelector('ul');
497 dependentList.textContent = '';
457 var dependentTemplate = $('template-collection').querySelector( 498 var dependentTemplate = $('template-collection').querySelector(
458 '.dependent-list-item'); 499 '.dependent-list-item');
459 extension.dependentExtensions.forEach(function(elem) { 500 extension.dependentExtensions.forEach(function(elem) {
460 var depNode = dependentTemplate.cloneNode(true); 501 var depNode = dependentTemplate.cloneNode(true);
461 depNode.querySelector('.dep-extension-title').textContent = elem.name; 502 depNode.querySelector('.dep-extension-title').textContent = elem.name;
462 depNode.querySelector('.dep-extension-id').textContent = elem.id; 503 depNode.querySelector('.dep-extension-id').textContent = elem.id;
463 dependentList.appendChild(depNode); 504 dependentList.appendChild(depNode);
464 }); 505 });
465 } 506 });
466 507
467 // Then active views. 508 // The active views.
468 if (extension.views.length > 0) { 509 this.updateVisibility_(node, '.active-views', extension.views.length > 0,
469 var activeViews = node.querySelector('.active-views'); 510 function(item) {
470 activeViews.hidden = false; 511 var link = item.querySelector('a');
471 var link = activeViews.querySelector('a'); 512
513 // Link needs to be an only child before the list is updated.
514 while (link.nextElementSibling)
515 item.removeChild(link.nextElementSibling);
516
517 // Link needs to be cleaned up if it was used before.
518 link.textContent = '';
519 if (link.clickHandler)
520 link.removeEventListener('click', link.clickHandler);
472 521
473 extension.views.forEach(function(view, i) { 522 extension.views.forEach(function(view, i) {
474 var displayName = view.generatedBackgroundPage ? 523 var displayName = view.generatedBackgroundPage ?
475 loadTimeData.getString('backgroundPage') : view.path; 524 loadTimeData.getString('backgroundPage') : view.path;
476 var label = displayName + 525 var label = displayName +
477 (view.incognito ? 526 (view.incognito ?
478 ' ' + loadTimeData.getString('viewIncognito') : '') + 527 ' ' + loadTimeData.getString('viewIncognito') : '') +
479 (view.renderProcessId == -1 ? 528 (view.renderProcessId == -1 ?
480 ' ' + loadTimeData.getString('viewInactive') : ''); 529 ' ' + loadTimeData.getString('viewInactive') : '');
481 link.textContent = label; 530 link.textContent = label;
482 link.addEventListener('click', function(e) { 531 link.clickHandler = function(e) {
483 // TODO(estade): remove conversion to string? 532 // TODO(estade): remove conversion to string?
484 chrome.send('extensionSettingsInspect', [ 533 chrome.send('extensionSettingsInspect', [
485 String(extension.id), 534 String(extension.id),
486 String(view.renderProcessId), 535 String(view.renderProcessId),
487 String(view.renderViewId), 536 String(view.renderViewId),
488 view.incognito 537 view.incognito
489 ]); 538 ]);
490 }); 539 };
540 link.addEventListener('click', link.clickHandler);
491 541
492 if (i < extension.views.length - 1) { 542 if (i < extension.views.length - 1) {
493 link = link.cloneNode(true); 543 link = link.cloneNode(true);
494 activeViews.appendChild(link); 544 item.appendChild(link);
495 } 545 }
496 }); 546 });
497 } 547 });
498 548
499 // The extension warnings (describing runtime issues). 549 // The extension warnings (describing runtime issues).
500 if (extension.warnings) { 550 this.updateVisibility_(node, '.extension-warnings', extension.warnings,
501 var panel = node.querySelector('.extension-warnings'); 551 function(item) {
502 panel.hidden = false; 552 var warningList = item.querySelector('ul');
503 var list = panel.querySelector('ul'); 553 warningList.textContent = '';
504 extension.warnings.forEach(function(warning) { 554 extension.warnings.forEach(function(warning) {
505 list.appendChild(document.createElement('li')).innerText = warning; 555 var li = document.createElement('li');
556 warningList.appendChild(li).innerText = warning;
506 }); 557 });
507 } 558 });
508 559
509 // If the ErrorConsole is enabled, we should have manifest and/or runtime 560 // If the ErrorConsole is enabled, we should have manifest and/or runtime
510 // errors. Otherwise, we may have install warnings. We should not have 561 // errors. Otherwise, we may have install warnings. We should not have
511 // both ErrorConsole errors and install warnings. 562 // both ErrorConsole errors and install warnings.
512 if (extension.manifestErrors) { 563 // Errors.
513 var panel = node.querySelector('.manifest-errors'); 564 this.updateErrors_(node.querySelector('.manifest-errors'),
514 panel.hidden = false; 565 extension.manifestErrors);
515 panel.appendChild(new extensions.ExtensionErrorList( 566 this.updateErrors_(node.querySelector('.runtime-errors'),
516 extension.manifestErrors)); 567 extension.runtimeErrors);
517 }
518 if (extension.runtimeErrors) {
519 var panel = node.querySelector('.runtime-errors');
520 panel.hidden = false;
521 panel.appendChild(new extensions.ExtensionErrorList(
522 extension.runtimeErrors));
523 }
524 if (extension.installWarnings) {
525 var panel = node.querySelector('.install-warnings');
526 panel.hidden = false;
527 var list = panel.querySelector('ul');
528 extension.installWarnings.forEach(function(warning) {
529 var li = document.createElement('li');
530 li.innerText = warning.message;
531 list.appendChild(li);
532 });
533 }
534 568
535 this.appendChild(node); 569 // Install warnings.
570 this.updateVisibility_(node, '.install-warnings',
571 extension.installWarnings, function(item) {
572 var installWarningList = item.querySelector('ul');
573 installWarningList.textContent = '';
574 if (extension.installWarnings) {
575 extension.installWarnings.forEach(function(warning) {
576 var li = document.createElement('li');
577 li.innerText = warning.message;
578 installWarningList.appendChild(li);
579 });
580 }
581 });
582
536 if (location.hash.substr(1) == extension.id) { 583 if (location.hash.substr(1) == extension.id) {
537 // Scroll beneath the fixed header so that the extension is not 584 // Scroll beneath the fixed header so that the extension is not
538 // obscured. 585 // obscured.
539 var topScroll = node.offsetTop - $('page-header').offsetHeight; 586 var topScroll = node.offsetTop - $('page-header').offsetHeight;
540 var pad = parseInt(window.getComputedStyle(node, null).marginTop, 10); 587 var pad = parseInt(window.getComputedStyle(node, null).marginTop, 10);
541 if (!isNaN(pad)) 588 if (!isNaN(pad))
542 topScroll -= pad / 2; 589 topScroll -= pad / 2;
543 setScrollTopForDocument(document, topScroll); 590 setScrollTopForDocument(document, topScroll);
544 } 591 }
545 }, 592 },
546 593
547 /** 594 /**
595 * Adds a listener to an element.
596 * @param {string} type The type of listener to add.
597 * @param {Element} node Ancestor of the element specified by |query|.
598 * @param {string} query A query to select an element in |node|.
599 * @param {function({Event} e)} onclick The function that should be called
600 * on click.
601 * @private
602 */
603 addListener_: function(type, node, query, onclick) {
Dan Beam 2015/02/11 20:09:43 onclick -> handler
hcarmona 2015/02/11 23:36:47 Done.
604 node.querySelector(query).addEventListener(type, onclick);
605 },
606
607 /**
608 * Updates an element's textContent.
609 * @param {Element} node Ancestor of the element specified by |query|.
610 * @param {string} query A query to select an element in |node|.
611 * @param {string} textContent
612 * @private
613 */
614 setTextContent_: function(node, query, textContent) {
Dan Beam 2015/02/11 20:09:43 nit: setText_
hcarmona 2015/02/11 23:36:46 Done.
615 node.querySelector(query).textContent = textContent;
616 },
617
618 /**
619 * Updates an element's visibility and calls |callback| if it is visible.
620 * @param {Element} node Ancestor of the element specified by |query|.
621 * @param {string} query A query to select an element in |node|.
622 * @param {boolean} visible Whether the element should be visible or not.
623 * @param {function({Element} item)} callback Callback if the element
624 * is visible. |item| is the element specified by |query|.
Dan Beam 2015/02/11 20:09:43 this is kind of an odd API...
hcarmona 2015/02/11 23:36:47 Should we rename it?
625 * @private
626 */
627 updateVisibility_: function(node, query, visible, callback) {
Dan Beam 2015/02/11 20:09:43 nit: showingCallback or shownCallback or something
hcarmona 2015/02/11 23:36:47 Done.
628 var item = node.querySelector(query);
Dan Beam 2015/02/11 20:09:43 nit: var item = assert(node.querySelector(query));
hcarmona 2015/02/11 23:36:47 Done.
629 assert(item);
630 item.hidden = !visible;
631 if (visible && callback)
632 callback(item);
633 },
634
635 /**
636 * Updates an element to show a list of errors.
637 * @param {Element} panel An element to hold the errors.
638 * @param {?Array<RuntimeError>} errors The errors to be displayed.
639 * @private
640 */
641 updateErrors_: function(panel, errors) {
642 // TODO(hcarmona): Look into updating the ExtensionErrorList rather than
643 // rebuilding it every time.
644 panel.hidden = !errors;
Dan Beam 2015/02/11 20:09:43 is this meant to only hide the pane if |errors| is
hcarmona 2015/02/11 23:36:47 Acknowledged.
645 panel.textContent = '';
646 if (errors)
Dan Beam 2015/02/11 20:09:42 i think this will work when errors == []. do you
hcarmona 2015/02/11 23:36:47 Changed to hide the panels if there's a list of 0
647 panel.appendChild(new extensions.ExtensionErrorList(errors));
648 },
649
650 /**
548 * Opens the extension options overlay for the extension with the given id. 651 * Opens the extension options overlay for the extension with the given id.
549 * @param {string} extensionId The id of extension whose options page should 652 * @param {string} extensionId The id of extension whose options page should
550 * be displayed. 653 * be displayed.
551 * @param {boolean} scroll Whether the page should scroll to the extension 654 * @param {boolean} scroll Whether the page should scroll to the extension
552 * @private 655 * @private
553 */ 656 */
554 showEmbeddedExtensionOptions_: function(extensionId, scroll) { 657 showEmbeddedExtensionOptions_: function(extensionId, scroll) {
555 if (this.optionsShown_) 658 if (this.optionsShown_)
556 return; 659 return;
557 660
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after
591 // TODO(dbeam): why do we need to focus <extensionoptions> before and 694 // TODO(dbeam): why do we need to focus <extensionoptions> before and
592 // after its showing animation? Makes very little sense to me. 695 // after its showing animation? Makes very little sense to me.
593 overlay.setInitialFocus(); 696 overlay.setInitialFocus();
594 }, 697 },
595 }; 698 };
596 699
597 return { 700 return {
598 ExtensionsList: ExtensionsList 701 ExtensionsList: ExtensionsList
599 }; 702 };
600 }); 703 });
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698