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

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: DNC - clean up enabled checkbox code 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
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 85 matching lines...) Expand 10 before | Expand all | Expand 10 after
96 96
97 /** 97 /**
98 * Indicates whether an embedded options page that was navigated to through 98 * 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 99 * the '?options=' URL query has been shown to the user. This is necessary
100 * to prevent showExtensionNodes_ from opening the options more than once. 100 * to prevent showExtensionNodes_ from opening the options more than once.
101 * @type {boolean} 101 * @type {boolean}
102 * @private 102 * @private
103 */ 103 */
104 optionsShown_: false, 104 optionsShown_: false,
105 105
106 /**
107 * Necessary to only show the butterbar once.
108 * @private {boolean}
109 */
110 butterbarShown_: false,
111
106 decorate: function() { 112 decorate: function() {
107 this.textContent = '';
108
109 this.showExtensionNodes_(); 113 this.showExtensionNodes_();
110 }, 114 },
111 115
112 getIdQueryParam_: function() { 116 getIdQueryParam_: function() {
113 return parseQueryParams(document.location)['id']; 117 return parseQueryParams(document.location)['id'];
114 }, 118 },
115 119
116 getOptionsQueryParam_: function() { 120 getOptionsQueryParam_: function() {
117 return parseQueryParams(document.location)['options']; 121 return parseQueryParams(document.location)['options'];
118 }, 122 },
119 123
120 /** 124 /**
121 * Creates all extension items from scratch. 125 * Creates all extension items from scratch.
122 * @private 126 * @private
123 */ 127 */
124 showExtensionNodes_: function() { 128 showExtensionNodes_: function() {
125 // Iterate over the extension data and add each item to the list. 129 // Iterate over the extension data and add each item to the list.
126 this.data_.extensions.forEach(this.createNode_, this); 130 this.data_.extensions.forEach(function(extension) {
131 var node = $(extension.id);
132
133 if (node)
134 this.updateNode_(extension, node);
135 else
136 this.createNode_(extension);
137 }, this);
127 138
128 var idToHighlight = this.getIdQueryParam_(); 139 var idToHighlight = this.getIdQueryParam_();
129 if (idToHighlight && $(idToHighlight)) 140 if (idToHighlight && $(idToHighlight))
130 this.scrollToNode_(idToHighlight); 141 this.scrollToNode_(idToHighlight);
131 142
132 var idToOpenOptions = this.getOptionsQueryParam_(); 143 var idToOpenOptions = this.getOptionsQueryParam_();
133 if (idToOpenOptions && $(idToOpenOptions)) 144 if (idToOpenOptions && $(idToOpenOptions))
134 this.showEmbeddedExtensionOptions_(idToOpenOptions, true); 145 this.showEmbeddedExtensionOptions_(idToOpenOptions, true);
135 146
136 if (this.data_.extensions.length == 0) 147 if (this.data_.extensions.length == 0)
(...skipping 11 matching lines...) Expand all
148 // Scroll offset should be calculated slightly higher than the actual 159 // Scroll offset should be calculated slightly higher than the actual
149 // offset of the element being scrolled to, so that it ends up not all 160 // offset of the element being scrolled to, so that it ends up not all
150 // the way at the top. That way it is clear that there are more elements 161 // the way at the top. That way it is clear that there are more elements
151 // above the element being scrolled to. 162 // above the element being scrolled to.
152 var scrollFudge = 1.2; 163 var scrollFudge = 1.2;
153 var scrollTop = $(extensionId).offsetTop - scrollFudge * 164 var scrollTop = $(extensionId).offsetTop - scrollFudge *
154 $(extensionId).clientHeight; 165 $(extensionId).clientHeight;
155 setScrollTopForDocument(document, scrollTop); 166 setScrollTopForDocument(document, scrollTop);
156 }, 167 },
157 168
169 // TODO(hcarmona): Remove this temporary hack before clicking commit.
170 <include src="create_node.js">
171 <include src="update_node.js">
172
158 /** 173 /**
159 * Synthesizes and initializes an HTML element for the extension metadata 174 * Adds a listener to an element.
160 * given in |extension|. 175 * @param {string} type The type of listener to add.
161 * @param {ExtensionData} extension A dictionary of extension metadata. 176 * @param {Element} node Ancestor of the element specified by |query|.
177 * @param {string} query A query to select an element in |node|.
178 * @param {function({Event} e)} onclick The function that should be called
179 * on click.
162 * @private 180 * @private
163 */ 181 */
164 createNode_: function(extension) { 182 addListener_: function(type, node, query, onclick) {
165 var template = $('template-collection').querySelector( 183 node.querySelector(query).addEventListener(type, onclick);
166 '.extension-list-item-wrapper');
167 var node = template.cloneNode(true);
168 node.id = extension.id;
169
170 if (!extension.enabled || extension.terminated)
171 node.classList.add('inactive-extension');
172
173 var classes = [];
174 if (extension.managedInstall) {
175 classes.push('policy-controlled', 'may-not-modify');
176 } else if (extension.dependentExtensions.length > 0) {
177 classes.push('may-not-remove', 'may-not-modify');
178 } else if (extension.recommendedInstall) {
179 classes.push('may-not-remove');
180 } else if (extension.suspiciousInstall ||
181 extension.corruptInstall ||
182 extension.updateRequiredByPolicy) {
183 classes.push('may-not-modify');
184 }
185 node.classList.add.apply(node.classList, classes);
186
187 var idToHighlight = this.getIdQueryParam_();
188 if (node.id == idToHighlight)
189 node.classList.add('extension-highlight');
190
191 var item = node.querySelector('.extension-list-item');
192 // Prevent the image cache of extension icon by using the reloaded
193 // timestamp as a query string. The timestamp is recorded when the user
194 // clicks the 'Reload' link. http://crbug.com/159302.
195 if (extensionReloadedTimestamp[extension.id]) {
196 item.style.backgroundImage =
197 'url(' + extension.icon + '?' +
198 extensionReloadedTimestamp[extension.id] + ')';
199 } else {
200 item.style.backgroundImage = 'url(' + extension.icon + ')';
201 }
202
203 var title = node.querySelector('.extension-title');
204 title.textContent = extension.name;
205
206 var version = node.querySelector('.extension-version');
207 version.textContent = extension.version;
208
209 var locationText = node.querySelector('.location-text');
210 locationText.textContent = extension.locationText;
211
212 var blacklistText = node.querySelector('.blacklist-text');
213 blacklistText.textContent = extension.blacklistText;
214
215 var description = document.createElement('span');
216 description.textContent = extension.description;
217 node.querySelector('.extension-description').appendChild(description);
218
219 // The 'Show Browser Action' button.
220 if (extension.enable_show_button) {
221 var showButton = node.querySelector('.show-button');
222 showButton.addEventListener('click', function(e) {
223 chrome.send('extensionSettingsShowButton', [extension.id]);
224 });
225 showButton.hidden = false;
226 }
227
228 // The 'allow in incognito' checkbox.
229 node.querySelector('.incognito-control').hidden =
230 !this.data_.incognitoAvailable;
231 var incognito = node.querySelector('.incognito-control input');
232 incognito.disabled = !extension.incognitoCanBeEnabled;
233 incognito.checked = extension.enabledIncognito;
234 if (!incognito.disabled) {
235 incognito.addEventListener('change', function(e) {
236 var checked = e.target.checked;
237 butterBarVisibility[extension.id] = checked;
238 butterBar.hidden = !checked || extension.is_hosted_app;
239 chrome.send('extensionSettingsEnableIncognito',
240 [extension.id, String(checked)]);
241 });
242 }
243 var butterBar = node.querySelector('.butter-bar');
244 butterBar.hidden = !butterBarVisibility[extension.id];
245
246 // The 'collect errors' checkbox. This should only be visible if the
247 // error console is enabled - we can detect this by the existence of the
248 // |errorCollectionEnabled| property.
249 if (extension.wantsErrorCollection) {
250 node.querySelector('.error-collection-control').hidden = false;
251 var errorCollection =
252 node.querySelector('.error-collection-control input');
253 errorCollection.checked = extension.errorCollectionEnabled;
254 errorCollection.addEventListener('change', function(e) {
255 chrome.send('extensionSettingsEnableErrorCollection',
256 [extension.id, String(e.target.checked)]);
257 });
258 }
259
260 // The 'allow on all urls' checkbox. This should only be visible if
261 // active script restrictions are enabled. If they are not enabled, no
262 // extensions should want all urls.
263 if (extension.wantsAllUrls) {
264 var allUrls = node.querySelector('.all-urls-control');
265 allUrls.addEventListener('click', function(e) {
266 chrome.send('extensionSettingsAllowOnAllUrls',
267 [extension.id, String(e.target.checked)]);
268 });
269 allUrls.querySelector('input').checked = extension.allowAllUrls;
270 allUrls.hidden = false;
271 }
272
273 // The 'allow file:// access' checkbox.
274 if (extension.wantsFileAccess) {
275 var fileAccess = node.querySelector('.file-access-control');
276 fileAccess.addEventListener('click', function(e) {
277 chrome.send('extensionSettingsAllowFileAccess',
278 [extension.id, String(e.target.checked)]);
279 });
280 fileAccess.querySelector('input').checked = extension.allowFileAccess;
281 fileAccess.hidden = false;
282 }
283
284 // The 'Options' button or link, depending on its behaviour.
285 if (extension.enabled && extension.optionsUrl) {
286 var options, optionsClickListener;
287 if (extension.optionsOpenInTab) {
288 options = node.querySelector('.options-link');
289 // Set an href to get the correct mouse-over appearance (link,
290 // footer) - but the actual link opening is done through chrome.send
291 // with a preventDefault().
292 options.setAttribute('href', extension.optionsPageHref);
293 optionsClickListener = function() {
294 chrome.send('extensionSettingsOptions', [extension.id]);
295 };
296 } else {
297 options = node.querySelector('.options-button');
298 optionsClickListener = function() {
299 this.showEmbeddedExtensionOptions_(extension.id, false);
300 }.bind(this);
301 }
302 options.addEventListener('click', function(e) {
303 optionsClickListener();
304 e.preventDefault();
305 });
306 options.hidden = false;
307 }
308
309 // The 'Permissions' link.
310 var permissions = node.querySelector('.permissions-link');
311 permissions.addEventListener('click', function(e) {
312 chrome.send('extensionSettingsPermissions', [extension.id]);
313 e.preventDefault();
314 });
315
316 // The 'View in Web Store/View Web Site' link.
317 if (extension.homepageUrl && !extension.enableExtensionInfoDialog) {
318 var siteLink = node.querySelector('.site-link');
319 siteLink.href = extension.homepageUrl;
320 siteLink.textContent = loadTimeData.getString(
321 extension.homepageProvided ? 'extensionSettingsVisitWebsite' :
322 'extensionSettingsVisitWebStore');
323 siteLink.hidden = false;
324 }
325
326 if (extension.allow_reload) {
327 // The 'Reload' link.
328 var reload = node.querySelector('.reload-link');
329 reload.addEventListener('click', function(e) {
330 chrome.send('extensionSettingsReload', [extension.id]);
331 extensionReloadedTimestamp[extension.id] = Date.now();
332 });
333 reload.hidden = false;
334
335 if (extension.is_platform_app) {
336 // The 'Launch' link.
337 var launch = node.querySelector('.launch-link');
338 launch.addEventListener('click', function(e) {
339 chrome.send('extensionSettingsLaunch', [extension.id]);
340 });
341 launch.hidden = false;
342 }
343 }
344
345 if (extension.terminated) {
346 var terminatedReload = node.querySelector('.terminated-reload-link');
347 terminatedReload.hidden = false;
348 terminatedReload.onclick = function() {
349 chrome.send('extensionSettingsReload', [extension.id]);
350 };
351 } else if (extension.corruptInstall && extension.isFromStore) {
352 var repair = node.querySelector('.corrupted-repair-button');
353 repair.hidden = false;
354 repair.onclick = function() {
355 chrome.send('extensionSettingsRepair', [extension.id]);
356 };
357 } else {
358 // The 'Enabled' checkbox.
359 var enable = node.querySelector('.enable-checkbox');
360 enable.hidden = false;
361 var enableCheckboxDisabled = extension.managedInstall ||
362 extension.suspiciousInstall ||
363 extension.corruptInstall ||
364 extension.updateRequiredByPolicy ||
365 extension.dependentExtensions.length > 0;
366 enable.querySelector('input').disabled = enableCheckboxDisabled;
367
368 if (extension.managedInstall) {
369 var indicator = new cr.ui.ControlledIndicator();
370 indicator.classList.add('controlled-extension-indicator');
371 indicator.setAttribute('controlled-by', 'policy');
372 indicator.setAttribute('textpolicy', extension.policyText || '');
373 node.querySelector('.enable-controls').appendChild(indicator);
374 }
375
376 if (!enableCheckboxDisabled) {
377 enable.addEventListener('click', function(e) {
378 // When e.target is the label instead of the checkbox, it doesn't
379 // have the checked property and the state of the checkbox is
380 // left unchanged.
381 var checked = e.target.checked;
382 if (checked == undefined)
383 checked = !e.currentTarget.querySelector('input').checked;
384 chrome.send('extensionSettingsEnable',
385 [extension.id, checked ? 'true' : 'false']);
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
409 // Developer mode ////////////////////////////////////////////////////////
410
411 // First we have the id.
412 var idLabel = node.querySelector('.extension-id');
413 idLabel.textContent = ' ' + extension.id;
414
415 // Then the path, if provided by unpacked extension.
416 if (extension.isUnpacked) {
417 var loadPath = node.querySelector('.load-path');
418 loadPath.hidden = false;
419 var pathLink = loadPath.querySelector('a:nth-of-type(1)');
420 pathLink.textContent = ' ' + extension.prettifiedPath;
421 pathLink.addEventListener('click', function(e) {
422 chrome.send('extensionSettingsShowPath', [String(extension.id)]);
423 e.preventDefault();
424 });
425 }
426
427 // Then the 'managed, cannot uninstall/disable' message.
428 if (extension.managedInstall || extension.recommendedInstall) {
429 node.querySelector('.managed-message').hidden = false;
430 } else {
431 if (extension.suspiciousInstall) {
432 // Then the 'This isn't from the webstore, looks suspicious' message.
433 node.querySelector('.suspicious-install-message').hidden = false;
434 }
435 if (extension.corruptInstall) {
436 // Then the 'This is a corrupt extension' message.
437 node.querySelector('.corrupt-install-message').hidden = false;
438 }
439 }
440
441 // Then the 'An update required by enterprise policy' message. Note that
442 // a force-installed extension might be disabled due to being outdated
443 // as well.
444 if (extension.updateRequiredByPolicy) {
445 node.querySelector('.update-required-message').hidden = false;
446 // We would like to hide managed installed message since this
447 // extension is disabled.
448 node.querySelector('.managed-message').hidden = true;
449 }
450
451 if (extension.dependentExtensions.length > 0) {
452 node.classList.add('developer-extras');
453 var dependentMessage =
454 node.querySelector('.dependent-extensions-message');
455 dependentMessage.hidden = false;
456 var dependentList = dependentMessage.querySelector('ul');
457 var dependentTemplate = $('template-collection').querySelector(
458 '.dependent-list-item');
459 extension.dependentExtensions.forEach(function(elem) {
460 var depNode = dependentTemplate.cloneNode(true);
461 depNode.querySelector('.dep-extension-title').textContent = elem.name;
462 depNode.querySelector('.dep-extension-id').textContent = elem.id;
463 dependentList.appendChild(depNode);
464 });
465 }
466
467 // Then active views.
468 if (extension.views.length > 0) {
469 var activeViews = node.querySelector('.active-views');
470 activeViews.hidden = false;
471 var link = activeViews.querySelector('a');
472
473 extension.views.forEach(function(view, i) {
474 var displayName = view.generatedBackgroundPage ?
475 loadTimeData.getString('backgroundPage') : view.path;
476 var label = displayName +
477 (view.incognito ?
478 ' ' + loadTimeData.getString('viewIncognito') : '') +
479 (view.renderProcessId == -1 ?
480 ' ' + loadTimeData.getString('viewInactive') : '');
481 link.textContent = label;
482 link.addEventListener('click', function(e) {
483 // TODO(estade): remove conversion to string?
484 chrome.send('extensionSettingsInspect', [
485 String(extension.id),
486 String(view.renderProcessId),
487 String(view.renderViewId),
488 view.incognito
489 ]);
490 });
491
492 if (i < extension.views.length - 1) {
493 link = link.cloneNode(true);
494 activeViews.appendChild(link);
495 }
496 });
497 }
498
499 // The extension warnings (describing runtime issues).
500 if (extension.warnings) {
501 var panel = node.querySelector('.extension-warnings');
502 panel.hidden = false;
503 var list = panel.querySelector('ul');
504 extension.warnings.forEach(function(warning) {
505 list.appendChild(document.createElement('li')).innerText = warning;
506 });
507 }
508
509 // If the ErrorConsole is enabled, we should have manifest and/or runtime
510 // errors. Otherwise, we may have install warnings. We should not have
511 // both ErrorConsole errors and install warnings.
512 if (extension.manifestErrors) {
513 var panel = node.querySelector('.manifest-errors');
514 panel.hidden = false;
515 panel.appendChild(new extensions.ExtensionErrorList(
516 extension.manifestErrors));
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
535 this.appendChild(node);
536 if (location.hash.substr(1) == extension.id) {
537 // Scroll beneath the fixed header so that the extension is not
538 // obscured.
539 var topScroll = node.offsetTop - $('page-header').offsetHeight;
540 var pad = parseInt(window.getComputedStyle(node, null).marginTop, 10);
541 if (!isNaN(pad))
542 topScroll -= pad / 2;
543 setScrollTopForDocument(document, topScroll);
544 }
545 }, 184 },
546 185
547 /** 186 /**
187 * Updates an element's textContent.
188 * @param {Element} node Ancestor of the element specified by |query|.
189 * @param {string} query A query to select an element in |node|.
190 * @param {string} textContent
191 * @private
192 */
193 setTextContent_: function(node, query, textContent) {
194 node.querySelector(query).textContent = textContent;
195 },
196
197 /**
198 * Updates an element's visibility and calls |callback| if it is visible.
199 * @param {Element} node Ancestor of the element specified by |query|.
200 * @param {string} query A query to select an element in |node|.
201 * @param {boolean} visible Whether the element should be visible or not.
202 * @param {function({Element} item)} callback Callback if the element
203 * is visible.
not at google - send to devlin 2015/02/10 22:14:37 Mention that the Element it's called with is the i
hcarmona 2015/02/11 02:22:07 Done.
204 * @private
205 */
206 updateElement_: function(node, query, visible, callback) {
not at google - send to devlin 2015/02/10 22:14:37 A more descriptive name would be updateVisibility
hcarmona 2015/02/11 02:22:07 Done.
207 var item = node.querySelector(query);
not at google - send to devlin 2015/02/10 22:14:37 Can you assert that the element existed?
hcarmona 2015/02/11 02:22:07 Done.
208 item.hidden = !visible;
209 if (visible && callback)
210 callback(item);
211 },
212
213 /**
214 * Updates an element to show a list of errors.
215 * @param {Element} panel An element to hold the errors.
216 * @param {?Array<RuntimeError>} errors The errors to be displayed.
217 * @private
218 */
219 updateErrors_: function(panel, errors) {
220 // TODO(hcarmona): Look into updating the ExtensionErrorList rather than
221 // rebuilding it every time.
222 panel.hidden = !errors;
223 panel.textContent = '';
224 if (errors)
225 panel.appendChild(new extensions.ExtensionErrorList(errors));
226 },
227
228 /**
548 * Opens the extension options overlay for the extension with the given id. 229 * Opens the extension options overlay for the extension with the given id.
549 * @param {string} extensionId The id of extension whose options page should 230 * @param {string} extensionId The id of extension whose options page should
550 * be displayed. 231 * be displayed.
551 * @param {boolean} scroll Whether the page should scroll to the extension 232 * @param {boolean} scroll Whether the page should scroll to the extension
552 * @private 233 * @private
553 */ 234 */
554 showEmbeddedExtensionOptions_: function(extensionId, scroll) { 235 showEmbeddedExtensionOptions_: function(extensionId, scroll) {
555 if (this.optionsShown_) 236 if (this.optionsShown_)
556 return; 237 return;
557 238
(...skipping 21 matching lines...) Expand all
579 $('overlay').addEventListener('cancelOverlay', function() { 260 $('overlay').addEventListener('cancelOverlay', function() {
580 this.optionsShown_ = false; 261 this.optionsShown_ = false;
581 }.bind(this)); 262 }.bind(this));
582 }, 263 },
583 }; 264 };
584 265
585 return { 266 return {
586 ExtensionsList: ExtensionsList 267 ExtensionsList: ExtensionsList
587 }; 268 };
588 }); 269 });
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698