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

Unified Diff: chrome/browser/resources/extensions/extension_list.js

Issue 916243002: Enable keyboard shortcuts for chrome://extensions (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Fix closure compile 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 side-by-side diff with in-line comments
Download patch
Index: chrome/browser/resources/extensions/extension_list.js
diff --git a/chrome/browser/resources/extensions/extension_list.js b/chrome/browser/resources/extensions/extension_list.js
index d4fcbf6babcd7bc2401013600cdcb86a2872a8c5..1ff5d98e7aaf25b571bbcb1517bf109c84046515 100644
--- a/chrome/browser/resources/extensions/extension_list.js
+++ b/chrome/browser/resources/extensions/extension_list.js
@@ -65,6 +65,121 @@
*/
var ExtensionData;
+///////////////////////////////////////////////////////////////////////////////
+// ExtensionFocusRow:
+
+/**
+ * Provides an implementation for a single column grid.
+ * @constructor
+ * @extends {cr.ui.FocusRow}
+ */
+function ExtensionFocusRow() {}
+
+/**
+ * Decorates |focusRow| so that it can be treated as a ExtensionFocusRow.
+ * @param {Element} focusRow The element that has all the columns.
+ * @param {Node} boundary Focus events are ignored outside of this node.
+ */
+ExtensionFocusRow.decorate = function(focusRow, boundary) {
+ focusRow.__proto__ = ExtensionFocusRow.prototype;
+ focusRow.decorate(boundary);
+};
+
+ExtensionFocusRow.prototype = {
+ __proto__: cr.ui.FocusRow.prototype,
+
+ /** @override */
+ getEquivalentElement: function(element) {
+ if (this.focusableElements.indexOf(element) > -1)
+ return element;
+
+ // All elements default to another element with the same type.
+ var columnType = element.getAttribute('column-type');
+ var equivalent = this.querySelector('[column-type=' + columnType + ']');
+
+ if (!equivalent || !this.canAddElement_(equivalent)) {
+ var actionLinks = ['options', 'website', 'launch', 'localReload'];
+ if (actionLinks.indexOf(columnType) > -1)
+ equivalent = this.getFirstFocusableByType_(actionLinks);
Dan Beam 2015/02/19 19:26:28 why are we potentially overwriting |equivalent| 4
hcarmona 2015/02/19 23:42:17 Only one should match. Changed to else if.
+
+ var optionalControls = ['showButton', 'incognito', 'dev-collectErrors',
+ 'allUrls', 'localUrls'];
+ if (optionalControls.indexOf(columnType) > -1)
+ equivalent = this.getFirstFocusableByType_(optionalControls);
+
+ var removeStyleButtons = ['trash', 'enterprise'];
+ if (removeStyleButtons.indexOf(columnType) > -1)
+ equivalent = this.getFirstFocusableByType_(removeStyleButtons);
+
+ var enableControls = ['terminatedReload', 'repair', 'enabled'];
+ if (enableControls.indexOf(columnType) > -1)
+ equivalent = this.getFirstFocusableByType_(enableControls);
+ }
+
+ // Return the first focusable element if no equivalent type is found.
+ return equivalent || this.focusableElements[0];
+ },
+
+ /** Updates the list of focusable elements. */
+ updateFocusableElements: function() {
+ this.focusableElements.length = 0;
+
+ var focusableCandidates = this.querySelectorAll('[column-type]');
+ for (var i = 0; i < focusableCandidates.length; ++i) {
+ var element = focusableCandidates[i];
+ if (this.canAddElement_(element))
+ this.addFocusableElement(element);
+ }
+ },
+
+ /**
+ * Get the first focusable element that matches a list of types.
+ * @param {Array<string>} types An array of types to match from.
+ * @return {?Element} Return the first element that matches a type in |types|.
+ * @private
+ */
+ getFirstFocusableByType_: function(types) {
+ for (var i = 0; i < this.focusableElements.length; ++i) {
+ var element = this.focusableElements[i];
+ if (types.indexOf(element.getAttribute('column-type')) > -1)
+ return element;
+ }
+ return null;
+ },
+
+ /**
+ * @param {Element} element
+ * @return {boolean}
+ * @private
+ */
+ canAddElement_: function(element) {
+ if (!element || element.disabled)
+ return false;
+
+ var developerMode = $('extension-settings').classList.contains('dev-mode');
+ if (this.isDeveloperOption_(element) && !developerMode)
+ return false;
+
+ for (var el = element; el; el = el.parentElement) {
+ if (el.hidden)
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * Returns true if the element should only be shown in developer mode.
+ * @param {Element} element
+ * @return {boolean}
+ * @private
+ */
+ isDeveloperOption_: function(element) {
+ var type = element.getAttribute('column-type');
+ return type.indexOf('dev-') == 0;
Dan Beam 2015/02/19 19:26:28 this can fit in one line as: return element.get
hcarmona 2015/02/19 23:42:17 Done.
+ },
+};
+
cr.define('options', function() {
'use strict';
@@ -96,6 +211,9 @@ cr.define('options', function() {
*/
optionsShown_: false,
+ /** @private {cr.ui.FocusGrid} */
Dan Beam 2015/02/19 19:26:28 nit: !cr.ui.FocusGrid
hcarmona 2015/02/19 23:42:17 Done.
+ focusGrid_: new cr.ui.FocusGrid(),
+
/**
* Necessary to only show the butterbar once.
* @private {boolean}
@@ -115,10 +233,13 @@ cr.define('options', function() {
},
/**
- * Creates all extension items from scratch.
+ * Creates or updates all extension items from scratch.
* @private
*/
showExtensionNodes_: function() {
+ // Remove the rows from |focusGrid_| without destroying them.
+ this.focusGrid_.rows.length = 0;
+
// Any node that is not updated will be removed.
var seenIds = [];
@@ -133,12 +254,18 @@ cr.define('options', function() {
this.createNode_(extension);
}, this);
- // Remove extensions that are no longer installed.
+ // Remove extensions that are no longer installed or add them to
+ // |focusGrid_| if they are still installed.
var nodes = document.querySelectorAll('.extension-list-item-wrapper[id]');
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
- if (seenIds.indexOf(node.id) < 0)
+ if (seenIds.indexOf(node.id) < 0) {
node.parentElement.removeChild(node);
+ // Unregister the removed node from events.
+ node.destroy();
+ } else {
+ this.focusGrid_.addRow(node);
+ }
}
var idToHighlight = this.getIdQueryParam_();
@@ -153,6 +280,14 @@ cr.define('options', function() {
this.classList.toggle('empty-extension-list', noExtensions);
},
+ /** Updates each row's focusable elements without rebuilding the grid. */
+ updateFocusableElements: function() {
+ var nodes = document.querySelectorAll('.extension-list-item-wrapper[id]');
Dan Beam 2015/02/19 19:26:28 nit: nodes -> rows
hcarmona 2015/02/19 23:42:17 Done.
+ for (var i = 0; i < nodes.length; ++i) {
+ nodes[i].updateFocusableElements();
+ }
+ },
+
/**
* Scrolls the page down to the extension node with the given id.
* @param {string} extensionId The id of the extension to scroll to.
@@ -179,15 +314,17 @@ cr.define('options', function() {
var template = $('template-collection').querySelector(
'.extension-list-item-wrapper');
var node = template.cloneNode(true);
+ ExtensionFocusRow.decorate(node, $('extension-settings-list'));
node.id = extension.id;
// The 'Show Browser Action' button.
- this.addListener_('click', node, '.show-button', function(e) {
+ this.addListener_('click', node, '.show-button', 'showButton',
+ function(e) {
chrome.send('extensionSettingsShowButton', [extension.id]);
});
// The 'allow in incognito' checkbox.
- this.addListener_('change', node, '.incognito-control input',
+ this.addListener_('change', node, '.incognito-control input', 'incognito',
function(e) {
var butterBar = node.querySelector('.butter-bar');
var checked = e.target.checked;
@@ -204,7 +341,7 @@ cr.define('options', function() {
// error console is enabled - we can detect this by the existence of the
// |errorCollectionEnabled| property.
this.addListener_('change', node, '.error-collection-control input',
- function(e) {
+ 'dev-collectErrors', function(e) {
chrome.send('extensionSettingsEnableErrorCollection',
[extension.id, String(e.target.checked)]);
});
@@ -212,13 +349,15 @@ cr.define('options', function() {
// The 'allow on all urls' checkbox. This should only be visible if
// active script restrictions are enabled. If they are not enabled, no
// extensions should want all urls.
- this.addListener_('click', node, '.all-urls-control', function(e) {
+ this.addListener_('click', node, '.all-urls-control input', 'allUrls',
+ function(e) {
chrome.send('extensionSettingsAllowOnAllUrls',
[extension.id, String(e.target.checked)]);
});
// The 'allow file:// access' checkbox.
- this.addListener_('click', node, '.file-access-control', function(e) {
+ this.addListener_('click', node, '.file-access-control input',
+ 'localUrls', function(e) {
chrome.send('extensionSettingsAllowFileAccess',
[extension.id, String(e.target.checked)]);
});
@@ -228,45 +367,54 @@ cr.define('options', function() {
// footer) - but the actual link opening is done through chrome.send
// with a preventDefault().
node.querySelector('.options-link').href = extension.optionsPageHref;
- this.addListener_('click', node, '.options-link', function(e) {
+ this.addListener_('click', node, '.options-link', 'options', function(e) {
chrome.send('extensionSettingsOptions', [extension.id]);
e.preventDefault();
});
- this.addListener_('click', node, '.options-button', function(e) {
+ this.addListener_('click', node, '.options-button', 'options',
+ function(e) {
this.showEmbeddedExtensionOptions_(extension.id, false);
e.preventDefault();
}.bind(this));
+ // The 'View in Web Store/View Web Site' link.
+ node.querySelector('.site-link').setAttribute('column-type', 'website');
+
// The 'Permissions' link.
- this.addListener_('click', node, '.permissions-link', function(e) {
+ this.addListener_('click', node, '.permissions-link', 'details',
+ function(e) {
chrome.send('extensionSettingsPermissions', [extension.id]);
e.preventDefault();
});
// The 'Reload' link.
- this.addListener_('click', node, '.reload-link', function(e) {
+ this.addListener_('click', node, '.reload-link', 'localReload',
+ function(e) {
chrome.send('extensionSettingsReload', [extension.id]);
extensionReloadedTimestamp[extension.id] = Date.now();
});
// The 'Launch' link.
- this.addListener_('click', node, '.launch-link', function(e) {
+ this.addListener_('click', node, '.launch-link', 'launch', function(e) {
chrome.send('extensionSettingsLaunch', [extension.id]);
});
// The 'Reload' terminated link.
- this.addListener_('click', node, '.terminated-reload-link', function(e) {
+ this.addListener_('click', node, '.terminated-reload-link',
+ 'terminatedReload', function(e) {
chrome.send('extensionSettingsReload', [extension.id]);
});
// The 'Repair' corrupted link.
- this.addListener_('click', node, '.corrupted-repair-button', function(e) {
+ this.addListener_('click', node, '.corrupted-repair-button', 'repair',
+ function(e) {
chrome.send('extensionSettingsRepair', [extension.id]);
});
// The 'Enabled' checkbox.
- this.addListener_('change', node, '.enable-checkbox input', function(e) {
+ this.addListener_('change', node, '.enable-checkbox input', 'enabled',
+ function(e) {
var checked = e.target.checked;
chrome.send('extensionSettingsEnable', [extension.id, String(checked)]);
@@ -282,6 +430,8 @@ cr.define('options', function() {
var trashTemplate = $('template-collection').querySelector('.trash');
var trash = trashTemplate.cloneNode(true);
trash.title = loadTimeData.getString('extensionUninstall');
+ trash.hidden = extension.managedInstall;
+ trash.setAttribute('column-type', 'trash');
trash.addEventListener('click', function(e) {
chrome.send('extensionSettingsUninstall', [extension.id]);
});
@@ -291,7 +441,7 @@ cr.define('options', function() {
// The path, if provided by unpacked extension.
this.addListener_('click', node, '.load-path a:first-of-type',
- function(e) {
+ 'dev-loadPath', function(e) {
chrome.send('extensionSettingsShowPath', [String(extension.id)]);
e.preventDefault();
});
@@ -307,8 +457,9 @@ cr.define('options', function() {
* @private
*/
updateNode_: function(extension, node) {
- node.classList.toggle('inactive-extension',
- !extension.enabled || extension.terminated);
+ var isActive = extension.enabled && !extension.terminated;
+
+ node.classList.toggle('inactive-extension', !isActive);
node.classList.remove('policy-controlled', 'may-not-modify',
'may-not-remove');
@@ -350,11 +501,12 @@ cr.define('options', function() {
// The 'Show Browser Action' button.
this.updateVisibility_(node, '.show-button',
- extension.enable_show_button);
+ isActive && extension.enable_show_button);
// The 'allow in incognito' checkbox.
this.updateVisibility_(node, '.incognito-control',
- this.data_.incognitoAvailable, function(item) {
+ isActive && this.data_.incognitoAvailable,
+ function(item) {
var incognito = item.querySelector('input');
incognito.disabled = !extension.incognitoCanBeEnabled;
incognito.checked = extension.enabledIncognito;
@@ -368,21 +520,23 @@ cr.define('options', function() {
// error console is enabled - we can detect this by the existence of the
// |errorCollectionEnabled| property.
this.updateVisibility_(node, '.error-collection-control',
- extension.wantsErrorCollection, function(item) {
+ isActive && extension.wantsErrorCollection,
+ function(item) {
item.querySelector('input').checked = extension.errorCollectionEnabled;
});
// The 'allow on all urls' checkbox. This should only be visible if
// active script restrictions are enabled. If they are not enabled, no
// extensions should want all urls.
- this.updateVisibility_(node, '.all-urls-control', extension.showAllUrls,
- function(item) {
+ this.updateVisibility_(node, '.all-urls-control',
+ isActive && extension.showAllUrls, function(item) {
item.querySelector('input').checked = extension.allowAllUrls;
});
// The 'allow file:// access' checkbox.
this.updateVisibility_(node, '.file-access-control',
- extension.wantsFileAccess, function(item) {
+ isActive && extension.wantsFileAccess,
+ function(item) {
item.querySelector('input').checked = extension.allowFileAccess;
});
@@ -446,6 +600,8 @@ cr.define('options', function() {
indicator.setAttribute('textpolicy', textPolicy);
indicator.image.setAttribute('aria-label', textPolicy);
controlNode.appendChild(indicator);
+ indicator.querySelector('div').setAttribute('column-type',
+ 'enterprise');
} else if (!needsIndicator && indicator) {
controlNode.removeChild(indicator);
}
@@ -540,6 +696,11 @@ cr.define('options', function() {
item.appendChild(link);
}
});
+
+ var allLinks = item.querySelectorAll('a');
+ for (var i = 0; i < allLinks.length; ++i) {
+ allLinks[i].setAttribute('column-type', 'dev-activeViews' + i);
+ }
});
// The extension warnings (describing runtime issues).
@@ -558,9 +719,9 @@ cr.define('options', function() {
// both ErrorConsole errors and install warnings.
// Errors.
this.updateErrors_(node.querySelector('.manifest-errors'),
- extension.manifestErrors);
+ 'dev-manifestErrors', extension.manifestErrors);
this.updateErrors_(node.querySelector('.runtime-errors'),
- extension.runtimeErrors);
+ 'dev-runtimeErrors', extension.runtimeErrors);
// Install warnings.
this.updateVisibility_(node, '.install-warnings',
@@ -585,6 +746,8 @@ cr.define('options', function() {
topScroll -= pad / 2;
setScrollTopForDocument(document, topScroll);
}
+
+ node.updateFocusableElements();
Dan Beam 2015/02/19 19:26:28 every time I see node#focusRowFunction() my eyes a
hcarmona 2015/02/19 23:42:17 Done.
},
/**
@@ -592,12 +755,16 @@ cr.define('options', function() {
* @param {string} type The type of listener to add.
* @param {Element} node Ancestor of the element specified by |query|.
* @param {string} query A query to select an element in |node|.
+ * @param {string} columnType A tag used to identify the column when
+ * changing focus.
* @param {function(Event)} handler The function that should be called
* on click.
* @private
*/
- addListener_: function(type, node, query, handler) {
- node.querySelector(query).addEventListener(type, handler);
+ addListener_: function(type, node, query, columnType, handler) {
+ var element = node.querySelector(query);
+ element.addEventListener(type, handler);
+ element.setAttribute('column-type', columnType);
Dan Beam 2015/02/19 19:26:28 this is an unexpected side effect in "addListener_
hcarmona 2015/02/19 23:42:17 Done.
},
/**
@@ -632,17 +799,29 @@ cr.define('options', function() {
/**
* Updates an element to show a list of errors.
* @param {Element} panel An element to hold the errors.
+ * @param {string} columnType A tag used to identify the column when
+ * changing focus.
* @param {Array<RuntimeError>|undefined} errors The errors to be displayed.
* @private
*/
- updateErrors_: function(panel, errors) {
+ updateErrors_: function(panel, columnType, errors) {
// TODO(hcarmona): Look into updating the ExtensionErrorList rather than
// rebuilding it every time.
panel.hidden = !errors || errors.length == 0;
panel.textContent = '';
if (!panel.hidden) {
- panel.appendChild(
- new extensions.ExtensionErrorList(assertInstanceof(errors, Array)));
+ var errorList =
+ new extensions.ExtensionErrorList(assertInstanceof(errors, Array));
+
+ panel.appendChild(errorList);
+
+ var list = errorList.getListElement();
+ if (list)
+ list.setAttribute('column-type', columnType + 'list');
+
+ var button = errorList.getToggleElement();
+ if (button)
+ button.setAttribute('column-type', columnType + 'button');
}
},

Powered by Google App Engine
This is Rietveld 408576698