diff options
| author | dbeam <dbeam@chromium.org> | 2015-08-13 20:20:07 -0700 |
|---|---|---|
| committer | Commit bot <commit-bot@chromium.org> | 2015-08-14 03:20:42 +0000 |
| commit | f9d4799b5b9c3f5c5694e24d3500a4bbe75950e9 (patch) | |
| tree | 1d4fb5bd4324e5b03f2be6bd6a0d174ecfda2a0f | |
| parent | f2a64e426b227a4c752be75034094b2a197288f1 (diff) | |
| download | chromium_src-f9d4799b5b9c3f5c5694e24d3500a4bbe75950e9.zip chromium_src-f9d4799b5b9c3f5c5694e24d3500a4bbe75950e9.tar.gz chromium_src-f9d4799b5b9c3f5c5694e24d3500a4bbe75950e9.tar.bz2 | |
More cr.ui.FocusRow simplifications
cr.ui.FocusRow no longer inherits from HTMLDivElement. Instead, it takes
a |root| argument in its constructor and uses |this.root| more.
Before there was lots of type variance. Something would start as a
<div>, then get decorated, but only work because cr.ui.FocusGrid derives
a <div>. This is clearer, more future-proof solution.
BUG=425626,518053
R=hcarmona@chromium.org,rdevlin.cronin@chromium.org
Review URL: https://codereview.chromium.org/1282043004
Cr-Commit-Position: refs/heads/master@{#343333}
| -rw-r--r-- | chrome/browser/resources/downloads/focus_row.js | 82 | ||||
| -rw-r--r-- | chrome/browser/resources/downloads/manager.js | 20 | ||||
| -rw-r--r-- | chrome/browser/resources/extensions/compiled_resources.gyp | 1 | ||||
| -rw-r--r-- | chrome/browser/resources/extensions/extension_error.js | 71 | ||||
| -rw-r--r-- | chrome/browser/resources/extensions/extension_list.js | 608 | ||||
| -rw-r--r-- | chrome/browser/resources/extensions/extensions.html | 2 | ||||
| -rw-r--r-- | chrome/browser/resources/extensions/extensions.js | 2 | ||||
| -rw-r--r-- | chrome/browser/resources/extensions/focus_row.js | 30 | ||||
| -rw-r--r-- | chrome/browser/resources/history/history.js | 110 | ||||
| -rw-r--r-- | chrome/browser/resources/history/other_devices.js | 44 | ||||
| -rw-r--r-- | ui/webui/resources/js/cr/ui/focus_grid.js | 16 | ||||
| -rw-r--r-- | ui/webui/resources/js/cr/ui/focus_row.js | 209 |
12 files changed, 545 insertions, 650 deletions
diff --git a/chrome/browser/resources/downloads/focus_row.js b/chrome/browser/resources/downloads/focus_row.js index c840132..fdebd6f 100644 --- a/chrome/browser/resources/downloads/focus_row.js +++ b/chrome/browser/resources/downloads/focus_row.js @@ -4,71 +4,29 @@ cr.define('downloads', function() { /** - * Provides an implementation for a single column grid. + * @param {!Element} root + * @param {?Node} boundary * @constructor * @extends {cr.ui.FocusRow} */ - function FocusRow() {} - - /** - * Decorates |focusRow| so that it can be treated as a FocusRow. - * @param {Element} focusRow The element that has all the columns represented - * by |itemView|. - * @param {!downloads.ItemView} itemView The item view this row cares about. - * @param {Node} boundary Focus events are ignored outside of this node. - */ - FocusRow.decorate = function(focusRow, itemView, boundary) { - focusRow.__proto__ = FocusRow.prototype; - focusRow.decorate(boundary); - focusRow.addFocusableElements_(); - }; - - FocusRow.prototype = { - __proto__: cr.ui.FocusRow.prototype, - - /** - * TODO(dbeam): remove all this :not([hidden]) hackery and just create 2 new - * methods on cr.ui.FocusRow that get possibly focusable nodes as well as - * currently focusable nodes (taking into account visibility). - * @override - */ - getEquivalentElement: function(element) { - if (this.focusableElements.indexOf(element) > -1 && - cr.ui.FocusRow.isFocusable(element)) { - return assert(element); - } - - // All elements default to another element with the same type. - var columnType = element.getAttribute('focus-type'); - var equivalent = this.querySelector( - '[focus-type=' + columnType + ']:not([hidden])'); - - if (this.focusableElements.indexOf(equivalent) < 0) { - equivalent = null; - var equivalentTypes = - ['show', 'retry', 'pause', 'resume', 'remove', 'cancel']; - if (equivalentTypes.indexOf(columnType) != -1) { - var allTypes = equivalentTypes.map(function(type) { - return '[focus-type=' + type + ']:not([hidden])'; - }).join(', '); - equivalent = this.querySelector(allTypes); - } - } - - // Return the first focusable element if no equivalent element is found. - return assert(equivalent || this.focusableElements[0]); - }, - - /** @private */ - addFocusableElements_: function() { - var possiblyFocusableElements = this.querySelectorAll('[focus-type]'); - for (var i = 0; i < possiblyFocusableElements.length; ++i) { - var possiblyFocusableElement = possiblyFocusableElements[i]; - if (cr.ui.FocusRow.isFocusable(possiblyFocusableElement)) - this.addFocusableElement(possiblyFocusableElement); - } - }, - }; + function FocusRow(root, boundary) { + cr.ui.FocusRow.call(this, root, boundary); + + assert(this.addItem('name', '[is="action-link"].name')); + assert(this.addItem('url', '.src-url')); + assert(this.addItem('show-retry', '.safe .controls .show')); + assert(this.addItem('show-retry', '.retry')); + assert(this.addItem('pause-resume', '.pause')); + assert(this.addItem('pause-resume', '.resume')); + assert(this.addItem('remove', '.remove')); + assert(this.addItem('cancel', '.cancel')); + assert(this.addItem('restore-save', '.restore')); + assert(this.addItem('restore-save', '.save')); + assert(this.addItem('remove-discard', '.remove')); + assert(this.addItem('remove-discard', '.discard')); + } + + FocusRow.prototype = {__proto__: cr.ui.FocusRow.prototype}; return {FocusRow: FocusRow}; }); diff --git a/chrome/browser/resources/downloads/manager.js b/chrome/browser/resources/downloads/manager.js index a11d849..7f91b61 100644 --- a/chrome/browser/resources/downloads/manager.js +++ b/chrome/browser/resources/downloads/manager.js @@ -130,10 +130,10 @@ cr.define('downloads', function() { var item = this.idMap_[data.id]; item.update(data); - var focusRow = this.decorateItem_(item); - if (focusRow.contains(activeElement) && + if (item.node.contains(activeElement) && !cr.ui.FocusRow.isFocusable(activeElement)) { + var focusRow = this.focusGrid_.getRowForRoot(item.node); focusRow.getEquivalentElement(activeElement).focus(); } }, @@ -150,25 +150,17 @@ cr.define('downloads', function() { this.focusGrid_.destroy(); this.items_.forEach(function(item) { - var focusRow = this.decorateItem_(item); + var focusRow = new downloads.FocusRow(item.node, this.node_); + this.focusGrid_.addRow(focusRow); - if (focusRow.contains(activeElement) && + if (item.node.contains(activeElement) && !cr.ui.FocusRow.isFocusable(activeElement)) { focusRow.getEquivalentElement(activeElement).focus(); } }, this); - this.focusGrid_.ensureRowActive(); - }, - /** - * @param {!downloads.ItemView} item An item to decorate as a FocusRow. - * @return {!downloads.FocusRow} |item| decorated as a FocusRow. - * @private - */ - decorateItem_: function(item) { - downloads.FocusRow.decorate(item.node, item, this.node_); - return assertInstanceof(item.node, downloads.FocusRow); + this.focusGrid_.ensureRowActive(); }, /** diff --git a/chrome/browser/resources/extensions/compiled_resources.gyp b/chrome/browser/resources/extensions/compiled_resources.gyp index e6127bf..a40ae06 100644 --- a/chrome/browser/resources/extensions/compiled_resources.gyp +++ b/chrome/browser/resources/extensions/compiled_resources.gyp @@ -29,6 +29,7 @@ '../../../../ui/webui/resources/js/event_tracker.js', '../../../../ui/webui/resources/js/load_time_data.js', '../../../../ui/webui/resources/js/util.js', + 'focus_row.js', ], 'externs': [ '<(EXTERNS_DIR)/chrome_extensions.js', diff --git a/chrome/browser/resources/extensions/extension_error.js b/chrome/browser/resources/extensions/extension_error.js index dbcd3e2..9ad71e8 100644 --- a/chrome/browser/resources/extensions/extension_error.js +++ b/chrome/browser/resources/extensions/extension_error.js @@ -43,35 +43,25 @@ cr.define('extensions', function() { * notification to the user when an error is caused by an extension. * @param {(RuntimeError|ManifestError)} error The error the element should * represent. - * @param {Element} boundary The boundary for the focus grid. * @constructor - * @extends {cr.ui.FocusRow} + * @extends {HTMLElement} */ - function ExtensionError(error, boundary) { + function ExtensionError(error) { var div = cloneTemplate('extension-error-metadata'); div.__proto__ = ExtensionError.prototype; - div.decorateWithError_(error, boundary); + div.decorate(error); return div; } ExtensionError.prototype = { - __proto__: cr.ui.FocusRow.prototype, - - /** @override */ - getEquivalentElement: function(element) { - return element.classList.contains('error-delete-button') ? - this.deleteButton_ : this.messageSpan_; - }, + __proto__: HTMLElement.prototype, /** * @param {(RuntimeError|ManifestError)} error The error the element should * represent. - * @param {Element} boundary The boundary for the FocusGrid. * @private */ - decorateWithError_: function(error, boundary) { - this.decorate(boundary); - + decorate: function(error) { /** * The backing error. * @type {(ManifestError|RuntimeError)} @@ -105,27 +95,25 @@ cr.define('extensions', function() { iconNode.alt = ''; this.insertBefore(iconNode, this.firstChild); - this.messageSpan_ = this.querySelector('.extension-error-message'); - this.messageSpan_.textContent = error.message; + var messageSpan = this.querySelector('.extension-error-message'); + messageSpan.textContent = error.message; - this.deleteButton_ = this.querySelector('.error-delete-button'); - this.deleteButton_.addEventListener('click', function(e) { + var deleteButton = this.querySelector('.error-delete-button'); + deleteButton.addEventListener('click', function(e) { this.dispatchEvent( new CustomEvent('deleteExtensionError', {bubbles: true, detail: this.error})); }.bind(this)); this.addEventListener('click', function(e) { - if (e.target != this.deleteButton_) + if (e.target != deleteButton) this.requestActive_(); }.bind(this)); + this.addEventListener('keydown', function(e) { - if (e.keyIdentifier == 'Enter' && e.target != this.deleteButton_) + if (e.keyIdentifier == 'Enter' && e.target != deleteButton) this.requestActive_(); }); - - this.addFocusableElement(this.messageSpan_); - this.addFocusableElement(this.deleteButton_); }, /** @@ -155,6 +143,23 @@ cr.define('extensions', function() { return div; } + /** + * @param {!Element} root + * @param {?Node} boundary + * @constructor + * @extends {cr.ui.FocusRow} + */ + ExtensionErrorList.FocusRow = function(root, boundary) { + cr.ui.FocusRow.call(this, root, boundary); + + this.addItem('message', '.extension-error-message'); + this.addItem('delete', '.error-delete-button'); + }; + + ExtensionErrorList.FocusRow.prototype = { + __proto__: cr.ui.FocusRow.prototype, + }; + ExtensionErrorList.prototype = { __proto__: HTMLDivElement.prototype, @@ -163,12 +168,13 @@ cr.define('extensions', function() { * @param {Array<(RuntimeError|ManifestError)>} errors The list of errors. */ decorate: function(errors) { - /** - * @private {!Array<(ManifestError|RuntimeError)>} - */ + /** @private {!Array<(ManifestError|RuntimeError)>} */ this.errors_ = []; + /** @private {!cr.ui.FocusGrid} */ this.focusGrid_ = new cr.ui.FocusGrid(); + + /** @private {Element} */ this.listContents_ = this.querySelector('.extension-error-list-contents'); errors.forEach(this.addError_, this); @@ -222,9 +228,12 @@ cr.define('extensions', function() { addError_: function(error) { this.querySelector('#no-errors-span').hidden = true; this.errors_.push(error); - var focusRow = new ExtensionError(error, this.listContents_); - this.listContents_.appendChild(focusRow); - this.focusGrid_.addRow(focusRow); + + var extensionError = new ExtensionError(error); + this.listContents_.appendChild(extensionError); + + this.focusGrid_.addRow( + new ExtensionErrorList.FocusRow(extensionError, this.listContents_)); }, /** @@ -247,7 +256,7 @@ cr.define('extensions', function() { this.errors_.splice(index, 1); var listElement = errorList.children[index]; - var focusRow = assertInstanceof(listElement, ExtensionError); + var focusRow = this.focusGrid_.getRowForRoot(listElement); this.focusGrid_.removeRow(focusRow); this.focusGrid_.ensureRowActive(); focusRow.destroy(); diff --git a/chrome/browser/resources/extensions/extension_list.js b/chrome/browser/resources/extensions/extension_list.js index 896f8c8..59984c6 100644 --- a/chrome/browser/resources/extensions/extension_list.js +++ b/chrome/browser/resources/extensions/extension_list.js @@ -4,148 +4,61 @@ <include src="extension_error.js"> -/////////////////////////////////////////////////////////////////////////////// -// 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('focus-type'); - var equivalent = this.querySelector('[focus-type=' + columnType + ']'); - - if (!equivalent || !this.canAddElement_(equivalent)) { - var actionLinks = ['options', 'website', 'launch', 'localReload']; - var optionalControls = ['showButton', 'incognito', 'dev-collectErrors', - 'allUrls', 'localUrls']; - var removeStyleButtons = ['trash', 'enterprise']; - var enableControls = ['terminatedReload', 'repair', 'enabled']; - - if (actionLinks.indexOf(columnType) > -1) - equivalent = this.getFirstFocusableByType_(actionLinks); - else if (optionalControls.indexOf(columnType) > -1) - equivalent = this.getFirstFocusableByType_(optionalControls); - else if (removeStyleButtons.indexOf(columnType) > -1) - equivalent = this.getFirstFocusableByType_(removeStyleButtons); - else 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]; - }, - - /** @override */ - makeActive: function(active) { - cr.ui.FocusRow.prototype.makeActive.call(this, active); - - // Only highlight if the row has focus. - this.classList.toggle('extension-highlight', - active && this.contains(document.activeElement)); - }, - - /** Updates the list of focusable elements. */ - updateFocusableElements: function() { - this.focusableElements.length = 0; - - var focusableCandidates = this.querySelectorAll('[focus-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('focus-type')) > -1) - return element; - } - return null; - }, +cr.define('extensions', function() { + 'use strict'; /** - * Setup a typical column in the ExtensionFocusRow. A column can be any - * element and should have an action when clicked/toggled. This function - * adds a listener and a handler for an event. Also adds the "focus-type" - * attribute to make the element focusable in |updateFocusableElements|. - * @param {string} query A query to select the element to set up. - * @param {string} columnType A tag used to identify the column when - * changing focus. - * @param {string} eventType The type of event to listen to. - * @param {function(Event)} handler The function that should be called - * by the event. - * @private + * @param {string} name The name of the template to clone. + * @return {!Element} The freshly cloned template. */ - setupColumn: function(query, columnType, eventType, handler) { - var element = this.querySelector(query); - element.addEventListener(eventType, handler); - element.setAttribute('focus-type', columnType); - }, + function cloneTemplate(name) { + var node = $('templates').querySelector('.' + name).cloneNode(true); + return assertInstanceof(node, Element); + } /** - * @param {Element} element - * @return {boolean} - * @private + * @extends {HTMLElement} + * @constructor */ - canAddElement_: function(element) { - if (!element || element.disabled) - return false; - - var developerMode = $('extension-settings').classList.contains('dev-mode'); - if (this.isDeveloperOption_(element) && !developerMode) - return false; + function ExtensionWrapper() { + var wrapper = cloneTemplate('extension-list-item-wrapper'); + wrapper.__proto__ = ExtensionWrapper.prototype; + wrapper.initialize(); + return wrapper; + } - for (var el = element; el; el = el.parentElement) { - if (el.hidden) - return false; - } + ExtensionWrapper.prototype = { + __proto__: HTMLElement.prototype, - return true; - }, + initialize: function() { + var boundary = $('extension-settings-list'); + /** @private {!extensions.FocusRow} */ + this.focusRow_ = new extensions.FocusRow(this, boundary); + }, - /** - * Returns true if the element should only be shown in developer mode. - * @param {Element} element - * @return {boolean} - * @private - */ - isDeveloperOption_: function(element) { - return /^dev-/.test(element.getAttribute('focus-type')); - }, -}; + /** @return {!cr.ui.FocusRow} */ + getFocusRow: function() { + return this.focusRow_; + }, -cr.define('extensions', function() { - 'use strict'; + /** + * Add an item to the focus row and listen for |eventType| events. + * @param {string} focusType A tag used to identify equivalent elements when + * changing focus between rows. + * @param {string} query A query to select the element to set up. + * @param {string=} opt_eventType The type of event to listen to. + * @param {function(Event)=} opt_handler The function that should be called + * by the event. + * @private + */ + setupColumn: function(focusType, query, opt_eventType, opt_handler) { + assert(this.focusRow_.addItem(focusType, query)); + if (opt_eventType) { + assert(opt_handler); + this.querySelector(query).addEventListener(opt_eventType, opt_handler); + } + }, + }; var ExtensionCommandsOverlay = extensions.ExtensionCommandsOverlay; @@ -276,14 +189,14 @@ cr.define('extensions', function() { case EventType.ERRORS_REMOVED: case EventType.PREFS_CHANGED: if (eventData.extensionInfo) { - this.updateExtension_(eventData.extensionInfo); + this.updateOrCreateWrapper_(eventData.extensionInfo); this.focusGrid_.ensureRowActive(); } break; case EventType.UNINSTALLED: var index = this.getIndexOfExtension_(eventData.item_id); this.extensions_.splice(index, 1); - this.removeNode_(getRequiredElement(eventData.item_id)); + this.removeWrapper_(getRequiredElement(eventData.item_id)); break; default: assertNotReached(); @@ -365,12 +278,18 @@ cr.define('extensions', function() { assert(!this.hidden); assert(!this.parentElement.hidden); - this.updateFocusableElements(); - var idToHighlight = this.getIdQueryParam_(); - if (idToHighlight && $(idToHighlight)) { - this.scrollToNode_(idToHighlight); - this.setInitialFocus_(idToHighlight); + if (idToHighlight) { + var wrapper = $(idToHighlight); + if (wrapper) { + this.scrollToWrapper_(idToHighlight); + + var focusRow = wrapper.getFocusRow(); + (focusRow.getFirstFocusable('enabled') || + focusRow.getFirstFocusable('remove-enterprise') || + focusRow.getFirstFocusable('website') || + focusRow.getFirstFocusable('details')).focus(); + } } var idToOpenOptions = this.getOptionsQueryParam_(); @@ -415,48 +334,46 @@ cr.define('extensions', function() { // Iterate over the extension data and add each item to the list. this.extensions_.forEach(function(extension) { seenIds.push(extension.id); - this.updateExtension_(extension); + this.updateOrCreateWrapper_(extension); }, this); this.focusGrid_.ensureRowActive(); // Remove extensions that are no longer installed. - var nodes = document.querySelectorAll('.extension-list-item-wrapper[id]'); - Array.prototype.forEach.call(nodes, function(node) { - if (seenIds.indexOf(node.id) < 0) - this.removeNode_(node); + var wrappers = document.querySelectorAll( + '.extension-list-item-wrapper[id]'); + Array.prototype.forEach.call(wrappers, function(wrapper) { + if (seenIds.indexOf(wrapper.id) < 0) + this.removeWrapper_(wrapper); }, this); }, - /** Updates each row's focusable elements without rebuilding the grid. */ - updateFocusableElements: function() { - var rows = document.querySelectorAll('.extension-list-item-wrapper[id]'); - for (var i = 0; i < rows.length; ++i) { - assertInstanceof(rows[i], ExtensionFocusRow).updateFocusableElements(); - } - }, - /** - * Removes the node from the DOM, and updates the focused element if needed. - * @param {!HTMLElement} node + * Removes the wrapper from the DOM and updates the focused element if + * needed. + * @param {!Element} wrapper * @private */ - removeNode_: function(node) { - if (node.contains(document.activeElement)) { - var nodes = - document.querySelectorAll('.extension-list-item-wrapper[id]'); - var index = Array.prototype.indexOf.call(nodes, node); + removeWrapper_: function(wrapper) { + // If focus is in the wrapper about to be removed, move it first. This + // happens when clicking the trash can to remove an extension. + if (wrapper.contains(document.activeElement)) { + var wrappers = document.querySelectorAll( + '.extension-list-item-wrapper[id]'); + var index = Array.prototype.indexOf.call(wrappers, wrapper); assert(index != -1); - var focusableNode = nodes[index + 1] || nodes[index - 1]; - if (focusableNode) - focusableNode.getEquivalentElement(document.activeElement).focus(); + var focusableWrapper = wrappers[index + 1] || wrappers[index - 1]; + if (focusableWrapper) { + var newFocusRow = focusableWrapper.getFocusRow(); + newFocusRow.getEquivalentElement(document.activeElement).focus(); + } } - node.parentNode.removeChild(node); - this.focusGrid_.removeRow(assertInstanceof(node, ExtensionFocusRow)); - - // Unregister the removed node from events. - assertInstanceof(node, ExtensionFocusRow).destroy(); + var focusRow = wrapper.getFocusRow(); + this.focusGrid_.removeRow(focusRow); this.focusGrid_.ensureRowActive(); + focusRow.destroy(); + + wrapper.parentNode.removeChild(wrapper); }, /** @@ -464,60 +381,95 @@ cr.define('extensions', function() { * @param {string} extensionId The id of the extension to scroll to. * @private */ - scrollToNode_: function(extensionId) { + scrollToWrapper_: function(extensionId) { // Scroll offset should be calculated slightly higher than the actual // offset of the element being scrolled to, so that it ends up not all // the way at the top. That way it is clear that there are more elements // above the element being scrolled to. + var wrapper = $(extensionId); var scrollFudge = 1.2; - var scrollTop = $(extensionId).offsetTop - scrollFudge * - $(extensionId).clientHeight; + var scrollTop = wrapper.offsetTop - scrollFudge * wrapper.clientHeight; setScrollTopForDocument(document, scrollTop); }, /** - * @param {string} extensionId The id of the extension that should have - * initial focus - * @private - */ - setInitialFocus_: function(extensionId) { - var focusRow = assertInstanceof($(extensionId), ExtensionFocusRow); - var columnTypePriority = ['enabled', 'enterprise', 'website', 'details']; - var elementToFocus = null; - var elementPriority = columnTypePriority.length; - - for (var i = 0; i < focusRow.focusableElements.length; ++i) { - var element = focusRow.focusableElements[i]; - var priority = - columnTypePriority.indexOf(element.getAttribute('focus-type')); - if (priority > -1 && priority < elementPriority) { - elementToFocus = element; - elementPriority = priority; - } - } - - focusRow.getEquivalentElement(elementToFocus).focus(); - }, - - /** * Synthesizes and initializes an HTML element for the extension metadata * given in |extension|. * @param {!ExtensionInfo} extension A dictionary of extension metadata. - * @param {?Element} nextNode |node| should be inserted before |nextNode|. - * |node| will be appended to the end if |nextNode| is null. + * @param {?Element} nextWrapper The newly created wrapper will be inserted + * before |nextWrapper| if non-null (else it will be appended to the + * wrapper list). * @private */ - createNode_: function(extension, nextNode) { - var template = $('template-collection').querySelector( - '.extension-list-item-wrapper'); - var node = template.cloneNode(true); - ExtensionFocusRow.decorate(node, $('extension-settings-list')); + createWrapper_: function(extension, nextWrapper) { + var wrapper = new ExtensionWrapper; + wrapper.id = extension.id; + + // The 'Permissions' link. + wrapper.setupColumn('details', '.permissions-link', 'click', function(e) { + if (!this.permissionsPromptIsShowing_) { + chrome.developerPrivate.showPermissionsDialog(extension.id, + function() { + this.permissionsPromptIsShowing_ = false; + }.bind(this)); + this.permissionsPromptIsShowing_ = true; + } + e.preventDefault(); + }); - var row = assertInstanceof(node, ExtensionFocusRow); - row.id = extension.id; + wrapper.setupColumn('options', '.options-button', 'click', function(e) { + this.showEmbeddedExtensionOptions_(extension.id, false); + e.preventDefault(); + }.bind(this)); + + // The 'Options' button or link, depending on its behaviour. + // Set an href to get the correct mouse-over appearance (link, + // footer) - but the actual link opening is done through developerPrivate + // API with a preventDefault(). + wrapper.querySelector('.options-link').href = + extension.optionsPage ? extension.optionsPage.url : ''; + wrapper.setupColumn('options', '.options-link', 'click', function(e) { + chrome.developerPrivate.showOptions(extension.id); + e.preventDefault(); + }); + + // The 'View in Web Store/View Web Site' link. + wrapper.setupColumn('website', '.site-link'); + + // The 'Launch' link. + wrapper.setupColumn('launch', '.launch-link', 'click', function(e) { + chrome.management.launchApp(extension.id); + }); + + // The 'Reload' link. + wrapper.setupColumn('localReload', '.reload-link', 'click', function(e) { + chrome.developerPrivate.reload(extension.id, {failQuietly: true}); + }); + + wrapper.setupColumn('errors', '.errors-link', 'click', function(e) { + var extensionId = extension.id; + assert(this.extensions_.length > 0); + var newEx = this.extensions_.filter(function(e) { + return e.state == chrome.developerPrivate.ExtensionState.ENABLED && + e.id == extensionId; + })[0]; + var errors = newEx.manifestErrors.concat(newEx.runtimeErrors); + extensions.ExtensionErrorOverlay.getInstance().setErrorsAndShowOverlay( + errors, extensionId, newEx.name); + }.bind(this)); + + wrapper.setupColumn('suspiciousLearnMore', + '.suspicious-install-message .learn-more-link'); + + // The path, if provided by unpacked extension. + wrapper.setupColumn('loadPath', '.load-path a:first-of-type', 'click', + function(e) { + chrome.developerPrivate.showPath(extension.id); + e.preventDefault(); + }); // The 'Show Browser Action' button. - row.setupColumn('.show-button', 'showButton', 'click', function(e) { + wrapper.setupColumn('showButton', '.show-button', 'click', function(e) { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: extension.id, showActionButton: true @@ -525,9 +477,9 @@ cr.define('extensions', function() { }); // The 'allow in incognito' checkbox. - row.setupColumn('.incognito-control input', 'incognito', 'change', - function(e) { - var butterBar = row.querySelector('.butter-bar'); + wrapper.setupColumn('incognito', '.incognito-control input', 'change', + function(e) { + var butterBar = wrapper.querySelector('.butter-bar'); var checked = e.target.checked; if (!this.butterbarShown_) { butterBar.hidden = !checked || @@ -546,8 +498,8 @@ cr.define('extensions', function() { // The 'collect errors' checkbox. This should only be visible if the // error console is enabled - we can detect this by the existence of the // |errorCollectionEnabled| property. - row.setupColumn('.error-collection-control input', 'dev-collectErrors', - 'change', function(e) { + wrapper.setupColumn('collectErrors', '.error-collection-control input', + 'change', function(e) { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: extension.id, errorCollection: e.target.checked @@ -557,8 +509,8 @@ cr.define('extensions', 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. - row.setupColumn('.all-urls-control input', 'allUrls', 'click', - function(e) { + wrapper.setupColumn('allUrls', '.all-urls-control input', 'click', + function(e) { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: extension.id, runOnAllUrls: e.target.checked @@ -566,82 +518,29 @@ cr.define('extensions', function() { }); // The 'allow file:// access' checkbox. - row.setupColumn('.file-access-control input', 'localUrls', 'click', - function(e) { + wrapper.setupColumn('localUrls', '.file-access-control input', 'click', + function(e) { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: extension.id, fileAccess: e.target.checked }); }); - // The 'Options' button or link, depending on its behaviour. - // Set an href to get the correct mouse-over appearance (link, - // footer) - but the actual link opening is done through developerPrivate - // API with a preventDefault(). - row.querySelector('.options-link').href = - extension.optionsPage ? extension.optionsPage.url : ''; - row.setupColumn('.options-link', 'options', 'click', function(e) { - chrome.developerPrivate.showOptions(extension.id); - e.preventDefault(); - }); - - row.setupColumn('.options-button', 'options', 'click', function(e) { - this.showEmbeddedExtensionOptions_(extension.id, false); - e.preventDefault(); - }.bind(this)); - - // The 'View in Web Store/View Web Site' link. - row.querySelector('.site-link').setAttribute('focus-type', 'website'); - - // The 'Permissions' link. - row.setupColumn('.permissions-link', 'details', 'click', function(e) { - if (!this.permissionsPromptIsShowing_) { - chrome.developerPrivate.showPermissionsDialog(extension.id, - function() { - this.permissionsPromptIsShowing_ = false; - }.bind(this)); - this.permissionsPromptIsShowing_ = true; - } - e.preventDefault(); - }); - - // The 'Reload' link. - row.setupColumn('.reload-link', 'localReload', 'click', function(e) { - chrome.developerPrivate.reload(extension.id, {failQuietly: true}); - }); - - // The 'Launch' link. - row.setupColumn('.launch-link', 'launch', 'click', function(e) { - chrome.management.launchApp(extension.id); - }); - - row.setupColumn('.errors-link', 'errors', 'click', function(e) { - var extensionId = extension.id; - assert(this.extensions_.length > 0); - var newEx = this.extensions_.filter(function(e) { - return e.state == chrome.developerPrivate.ExtensionState.ENABLED && - e.id == extensionId; - })[0]; - var errors = newEx.manifestErrors.concat(newEx.runtimeErrors); - extensions.ExtensionErrorOverlay.getInstance().setErrorsAndShowOverlay( - errors, extensionId, newEx.name); - }.bind(this)); - // The 'Reload' terminated link. - row.setupColumn('.terminated-reload-link', 'terminatedReload', 'click', - function(e) { + wrapper.setupColumn('terminatedReload', '.terminated-reload-link', + 'click', function(e) { chrome.developerPrivate.reload(extension.id, {failQuietly: true}); }); // The 'Repair' corrupted link. - row.setupColumn('.corrupted-repair-button', 'repair', 'click', - function(e) { + wrapper.setupColumn('repair', '.corrupted-repair-button', 'click', + function(e) { chrome.developerPrivate.repairExtension(extension.id); }); // The 'Enabled' checkbox. - row.setupColumn('.enable-checkbox input', 'enabled', 'change', - function(e) { + wrapper.setupColumn('enabled', '.enable-checkbox input', 'change', + function(e) { var checked = e.target.checked; // TODO(devlin): What should we do if this fails? chrome.management.setEnabled(extension.id, checked); @@ -655,11 +554,12 @@ cr.define('extensions', function() { }); // 'Remove' button. - var trashTemplate = $('template-collection').querySelector('.trash'); - var trash = trashTemplate.cloneNode(true); + var trash = cloneTemplate('trash'); trash.title = loadTimeData.getString('extensionUninstall'); - trash.setAttribute('focus-type', 'trash'); - trash.addEventListener('click', function(e) { + + wrapper.querySelector('.enable-controls').appendChild(trash); + + wrapper.setupColumn('remove-enterprise', '.trash', 'click', function(e) { trash.classList.add('open'); trash.classList.toggle('mouse-clicked', e.detail > 0); if (this.uninstallIsShowing_) @@ -683,69 +583,51 @@ cr.define('extensions', function() { } }.bind(this)); }.bind(this)); - row.querySelector('.enable-controls').appendChild(trash); - - // Developer mode //////////////////////////////////////////////////////// - - // The path, if provided by unpacked extension. - row.setupColumn('.load-path a:first-of-type', 'dev-loadPath', 'click', - function(e) { - chrome.developerPrivate.showPath(extension.id); - e.preventDefault(); - }); // Maintain the order that nodes should be in when creating as well as - // when adding only one new row. - this.insertBefore(row, nextNode); - this.updateNode_(extension, row); - - var nextRow = null; - if (nextNode) - nextRow = assertInstanceof(nextNode, ExtensionFocusRow); + // when adding only one new wrapper. + this.insertBefore(wrapper, nextWrapper); + this.updateWrapper_(extension, wrapper); - this.focusGrid_.addRowBefore(row, nextRow); + var nextRow = this.focusGrid_.getRowForRoot(nextWrapper); // May be null. + this.focusGrid_.addRowBefore(wrapper.getFocusRow(), nextRow); }, /** * Updates an HTML element for the extension metadata given in |extension|. * @param {!ExtensionInfo} extension A dictionary of extension metadata. - * @param {!ExtensionFocusRow} row The node that is being updated. + * @param {!Element} wrapper The extension wrapper element to update. * @private */ - updateNode_: function(extension, row) { + updateWrapper_: function(extension, wrapper) { var isActive = extension.state == chrome.developerPrivate.ExtensionState.ENABLED; - row.classList.toggle('inactive-extension', !isActive); + wrapper.classList.toggle('inactive-extension', !isActive); + wrapper.classList.remove('controlled', 'may-not-remove'); - // Hack to keep the closure compiler happy about |remove|. - // TODO(hcarmona): Remove this hack when the closure compiler is updated. - var node = /** @type {Element} */ (row); - node.classList.remove('controlled', 'may-not-remove'); - var classes = []; if (extension.controlledInfo) { - classes.push('controlled'); + wrapper.classList.add('controlled'); } else if (!extension.userMayModify || extension.mustRemainInstalled || extension.dependentExtensions.length > 0) { - classes.push('may-not-remove'); + wrapper.classList.add('may-not-remove'); } - row.classList.add.apply(row.classList, classes); - var item = row.querySelector('.extension-list-item'); + var item = wrapper.querySelector('.extension-list-item'); item.style.backgroundImage = 'url(' + extension.iconUrl + ')'; - this.setText_(row, '.extension-title', extension.name); - this.setText_(row, '.extension-version', extension.version); - this.setText_(row, '.location-text', extension.locationText || ''); - this.setText_(row, '.blacklist-text', extension.blacklistText || ''); - this.setText_(row, '.extension-description', extension.description); + this.setText_(wrapper, '.extension-title', extension.name); + this.setText_(wrapper, '.extension-version', extension.version); + this.setText_(wrapper, '.location-text', extension.locationText || ''); + this.setText_(wrapper, '.blacklist-text', extension.blacklistText || ''); + this.setText_(wrapper, '.extension-description', extension.description); // The 'Show Browser Action' button. - this.updateVisibility_(row, '.show-button', + this.updateVisibility_(wrapper, '.show-button', isActive && extension.actionButtonHidden); // The 'allow in incognito' checkbox. - this.updateVisibility_(row, '.incognito-control', + this.updateVisibility_(wrapper, '.incognito-control', isActive && this.incognitoAvailable_, function(item) { var incognito = item.querySelector('input'); @@ -754,7 +636,7 @@ cr.define('extensions', function() { }); // Hide butterBar if incognito is not enabled for the extension. - var butterBar = row.querySelector('.butter-bar'); + var butterBar = wrapper.querySelector('.butter-bar'); butterBar.hidden = butterBar.hidden || !extension.incognitoAccess.isEnabled; @@ -762,7 +644,7 @@ cr.define('extensions', function() { // error console is enabled - we can detect this by the existence of the // |errorCollectionEnabled| property. this.updateVisibility_( - row, '.error-collection-control', + wrapper, '.error-collection-control', isActive && extension.errorCollection.isEnabled, function(item) { item.querySelector('input').checked = @@ -773,14 +655,14 @@ cr.define('extensions', function() { // active script restrictions are enabled. If they are not enabled, no // extensions should want all urls. this.updateVisibility_( - row, '.all-urls-control', + wrapper, '.all-urls-control', isActive && extension.runOnAllUrls.isEnabled, function(item) { item.querySelector('input').checked = extension.runOnAllUrls.isActive; }); // The 'allow file:// access' checkbox. - this.updateVisibility_(row, '.file-access-control', + this.updateVisibility_(wrapper, '.file-access-control', isActive && extension.fileAccess.isEnabled, function(item) { item.querySelector('input').checked = extension.fileAccess.isActive; @@ -788,15 +670,15 @@ cr.define('extensions', function() { // The 'Options' button or link, depending on its behaviour. var optionsEnabled = isActive && !!extension.optionsPage; - this.updateVisibility_(row, '.options-link', optionsEnabled && + this.updateVisibility_(wrapper, '.options-link', optionsEnabled && extension.optionsPage.openInTab); - this.updateVisibility_(row, '.options-button', optionsEnabled && + this.updateVisibility_(wrapper, '.options-button', optionsEnabled && !extension.optionsPage.openInTab); // The 'View in Web Store/View Web Site' link. var siteLinkEnabled = !!extension.homePage.url && !this.enableAppInfoDialog_; - this.updateVisibility_(row, '.site-link', siteLinkEnabled, + this.updateVisibility_(wrapper, '.site-link', siteLinkEnabled, function(item) { item.href = extension.homePage.url; item.textContent = loadTimeData.getString( @@ -807,18 +689,19 @@ cr.define('extensions', function() { var isUnpacked = extension.location == chrome.developerPrivate.Location.UNPACKED; // The 'Reload' link. - this.updateVisibility_(row, '.reload-link', isUnpacked); + this.updateVisibility_(wrapper, '.reload-link', isUnpacked); // The 'Launch' link. this.updateVisibility_( - row, '.launch-link', + wrapper, '.launch-link', isUnpacked && extension.type == chrome.developerPrivate.ExtensionType.PLATFORM_APP); // The 'Errors' link. var hasErrors = extension.runtimeErrors.length > 0 || extension.manifestErrors.length > 0; - this.updateVisibility_(row, '.errors-link', hasErrors, function(item) { + this.updateVisibility_(wrapper, '.errors-link', hasErrors, + function(item) { var Level = chrome.developerPrivate.ErrorLevel; var map = {}; @@ -846,18 +729,18 @@ cr.define('extensions', function() { // The 'Reload' terminated link. var isTerminated = extension.state == chrome.developerPrivate.ExtensionState.TERMINATED; - this.updateVisibility_(row, '.terminated-reload-link', isTerminated); + this.updateVisibility_(wrapper, '.terminated-reload-link', isTerminated); // The 'Repair' corrupted link. var canRepair = !isTerminated && extension.disableReasons.corruptInstall && extension.location == chrome.developerPrivate.Location.FROM_STORE; - this.updateVisibility_(row, '.corrupted-repair-button', canRepair); + this.updateVisibility_(wrapper, '.corrupted-repair-button', canRepair); // The 'Enabled' checkbox. var isOK = !isTerminated && !canRepair; - this.updateVisibility_(row, '.enable-checkbox', isOK, function(item) { + this.updateVisibility_(wrapper, '.enable-checkbox', isOK, function(item) { var enableCheckboxDisabled = !extension.userMayModify || extension.disableReasons.suspiciousInstall || @@ -869,7 +752,7 @@ cr.define('extensions', function() { }); // Indicator for extensions controlled by policy. - var controlNode = row.querySelector('.enable-controls'); + var controlNode = wrapper.querySelector('.enable-controls'); var indicator = controlNode.querySelector('.controlled-extension-indicator'); var needsIndicator = isOK && extension.controlledInfo; @@ -895,8 +778,7 @@ cr.define('extensions', function() { indicator.setAttribute('text' + controlledByStr, text); indicator.image.setAttribute('aria-label', text); controlNode.appendChild(indicator); - indicator.querySelector('div').setAttribute('focus-type', - 'enterprise'); + wrapper.setupColumn('remove-enterprise', '[controlled-by] div'); } else if (!needsIndicator && indicator) { controlNode.removeChild(indicator); } @@ -904,11 +786,11 @@ cr.define('extensions', function() { // Developer mode //////////////////////////////////////////////////////// // First we have the id. - var idLabel = row.querySelector('.extension-id'); + var idLabel = wrapper.querySelector('.extension-id'); idLabel.textContent = ' ' + extension.id; // Then the path, if provided by unpacked extension. - this.updateVisibility_(row, '.load-path', isUnpacked, + this.updateVisibility_(wrapper, '.load-path', isUnpacked, function(item) { item.querySelector('a:first-of-type').textContent = ' ' + extension.prettifiedPath; @@ -919,32 +801,31 @@ cr.define('extensions', function() { // extension is disabled. var isRequired = !extension.userMayModify || extension.mustRemainInstalled; - this.updateVisibility_(row, '.managed-message', isRequired && + this.updateVisibility_(wrapper, '.managed-message', isRequired && !extension.disableReasons.updateRequired); // Then the 'This isn't from the webstore, looks suspicious' message. - this.updateVisibility_(row, '.suspicious-install-message', !isRequired && - extension.disableReasons.suspiciousInstall); + var isSuspicious = extension.disableReasons.suspiciousInstall; + this.updateVisibility_(wrapper, '.suspicious-install-message', + !isRequired && isSuspicious); // Then the 'This is a corrupt extension' message. - this.updateVisibility_(row, '.corrupt-install-message', !isRequired && + this.updateVisibility_(wrapper, '.corrupt-install-message', !isRequired && extension.disableReasons.corruptInstall); // Then the 'An update required by enterprise policy' message. Note that // a force-installed extension might be disabled due to being outdated // as well. - this.updateVisibility_(row, '.update-required-message', + this.updateVisibility_(wrapper, '.update-required-message', extension.disableReasons.updateRequired); // The 'following extensions depend on this extension' list. var hasDependents = extension.dependentExtensions.length > 0; - row.classList.toggle('developer-extras', hasDependents); - this.updateVisibility_(row, '.dependent-extensions-message', + wrapper.classList.toggle('developer-extras', hasDependents); + this.updateVisibility_(wrapper, '.dependent-extensions-message', hasDependents, function(item) { var dependentList = item.querySelector('ul'); dependentList.textContent = ''; - var dependentTemplate = $('template-collection').querySelector( - '.dependent-list-item'); extension.dependentExtensions.forEach(function(dependentId) { var dependentExtension = null; for (var i = 0; i < this.extensions_.length; ++i) { @@ -956,7 +837,7 @@ cr.define('extensions', function() { if (!dependentExtension) return; - var depNode = dependentTemplate.cloneNode(true); + var depNode = cloneTemplate('dependent-list-item'); depNode.querySelector('.dep-extension-title').textContent = dependentExtension.name; depNode.querySelector('.dep-extension-id').textContent = @@ -966,8 +847,8 @@ cr.define('extensions', function() { }.bind(this)); // The active views. - this.updateVisibility_(row, '.active-views', extension.views.length > 0, - function(item) { + this.updateVisibility_(wrapper, '.active-views', + extension.views.length > 0, function(item) { var link = item.querySelector('a'); // Link needs to be an only child before the list is updated. @@ -1013,16 +894,13 @@ cr.define('extensions', function() { link = link.cloneNode(true); item.appendChild(link); } - }); - var allLinks = item.querySelectorAll('a'); - for (var i = 0; i < allLinks.length; ++i) { - allLinks[i].setAttribute('focus-type', 'dev-activeViews' + i); - } + wrapper.setupColumn('activeView', '.active-views a:last-of-type'); + }); }); // The extension warnings (describing runtime issues). - this.updateVisibility_(row, '.extension-warnings', + this.updateVisibility_(wrapper, '.extension-warnings', extension.runtimeWarnings.length > 0, function(item) { var warningList = item.querySelector('ul'); @@ -1034,7 +912,7 @@ cr.define('extensions', function() { }); // Install warnings. - this.updateVisibility_(row, '.install-warnings', + this.updateVisibility_(wrapper, '.install-warnings', extension.installWarnings.length > 0, function(item) { var installWarningList = item.querySelector('ul'); @@ -1051,19 +929,17 @@ cr.define('extensions', function() { if (location.hash.substr(1) == extension.id) { // Scroll beneath the fixed header so that the extension is not // obscured. - var topScroll = row.offsetTop - $('page-header').offsetHeight; - var pad = parseInt(window.getComputedStyle(row, null).marginTop, 10); + var topScroll = wrapper.offsetTop - $('page-header').offsetHeight; + var pad = parseInt(window.getComputedStyle(wrapper).marginTop, 10); if (!isNaN(pad)) topScroll -= pad / 2; setScrollTopForDocument(document, topScroll); } - - row.updateFocusableElements(); }, /** * Updates an element's textContent. - * @param {Element} node Ancestor of the element specified by |query|. + * @param {Node} node Ancestor of the element specified by |query|. * @param {string} query A query to select an element in |node|. * @param {string} textContent * @private @@ -1075,7 +951,7 @@ cr.define('extensions', function() { /** * Updates an element's visibility and calls |shownCallback| if it is * visible. - * @param {Element} node Ancestor of the element specified by |query|. + * @param {Node} node Ancestor of the element specified by |query|. * @param {string} query A query to select an element in |node|. * @param {boolean} visible Whether the element should be visible or not. * @param {function(Element)=} opt_shownCallback Callback if the element is @@ -1084,10 +960,10 @@ cr.define('extensions', function() { * @private */ updateVisibility_: function(node, query, visible, opt_shownCallback) { - var item = assert(node.querySelector(query)); - item.hidden = !visible; + var element = assertInstanceof(node.querySelector(query), Element); + element.hidden = !visible; if (visible && opt_shownCallback) - opt_shownCallback(item); + opt_shownCallback(element); }, /** @@ -1112,7 +988,7 @@ cr.define('extensions', function() { return; if (scroll) - this.scrollToNode_(extensionId); + this.scrollToWrapper_(extensionId); // Add the options query string. Corner case: the 'options' query string // will clobber the 'id' query string if the options link is clicked when @@ -1161,12 +1037,12 @@ cr.define('extensions', function() { }, /** - * Updates the node for the extension. + * Updates or creates a wrapper for |extension|. * @param {!ExtensionInfo} extension The information about the extension to * update. * @private */ - updateExtension_: function(extension) { + updateOrCreateWrapper_: function(extension) { var currIndex = this.getIndexOfExtension_(extension.id); if (currIndex != -1) { // If there is a current version of the extension, update it with the @@ -1180,12 +1056,12 @@ cr.define('extensions', function() { this.extensions_.sort(compareExtensions); } - var node = /** @type {ExtensionFocusRow} */ ($(extension.id)); - if (node) { - this.updateNode_(extension, node); + var wrapper = $(extension.id); + if (wrapper) { + this.updateWrapper_(extension, wrapper); } else { var nextExt = this.extensions_[this.extensions_.indexOf(extension) + 1]; - this.createNode_(extension, nextExt ? $(nextExt.id) : null); + this.createWrapper_(extension, nextExt ? $(nextExt.id) : null); } } }; diff --git a/chrome/browser/resources/extensions/extensions.html b/chrome/browser/resources/extensions/extensions.html index 58bbf471..155714e 100644 --- a/chrome/browser/resources/extensions/extensions.html +++ b/chrome/browser/resources/extensions/extensions.html @@ -127,7 +127,7 @@ <span id="font-measuring-div"></span> -<div id="template-collection" hidden> +<div id="templates" hidden> <div class="extension-list-item-wrapper"> <div class="extension-list-item"> diff --git a/chrome/browser/resources/extensions/extensions.js b/chrome/browser/resources/extensions/extensions.js index 5868fca..3be5ca2 100644 --- a/chrome/browser/resources/extensions/extensions.js +++ b/chrome/browser/resources/extensions/extensions.js @@ -9,6 +9,7 @@ <include src="extension_commands_overlay.js"> <include src="extension_error_overlay.js"> <include src="extension_focus_manager.js"> +<include src="focus_row.js"> <include src="extension_list.js"> <include src="pack_extension_overlay.js"> <include src="extension_loader.js"> @@ -152,7 +153,6 @@ cr.define('extensions', function() { $('toggle-dev-on').addEventListener('change', function(e) { this.updateDevControlsVisibility_(true); - extensionList.updateFocusableElements(); chrome.developerPrivate.updateProfileConfiguration( {inDeveloperMode: e.target.checked}); var suffix = $('toggle-dev-on').checked ? 'Enabled' : 'Disabled'; diff --git a/chrome/browser/resources/extensions/focus_row.js b/chrome/browser/resources/extensions/focus_row.js new file mode 100644 index 0000000..6e9b078 --- /dev/null +++ b/chrome/browser/resources/extensions/focus_row.js @@ -0,0 +1,30 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('extensions', function() { + /** + * @param {!Element} root + * @param {Node} boundary + * @constructor + * @extends {cr.ui.FocusRow} + */ + function FocusRow(root, boundary) { + cr.ui.FocusRow.call(this, root, boundary); + } + + FocusRow.prototype = { + __proto__: cr.ui.FocusRow.prototype, + + /** @override */ + makeActive: function(active) { + cr.ui.FocusRow.prototype.makeActive.call(this, active); + + // Only highlight if the row has focus. + this.root.classList.toggle('extension-highlight', + active && this.root.contains(document.activeElement)); + }, + }; + + return {FocusRow: FocusRow}; +}); diff --git a/chrome/browser/resources/history/history.js b/chrome/browser/resources/history/history.js index d861b3f..40a3361 100644 --- a/chrome/browser/resources/history/history.js +++ b/chrome/browser/resources/history/history.js @@ -892,78 +892,48 @@ HistoryModel.prototype.getGroupByDomain = function() { /** * Provides an implementation for a single column grid. + * @param {!Element} root + * @param {Node} boundary * @constructor * @extends {cr.ui.FocusRow} */ -function HistoryFocusRow() {} +function HistoryFocusRow(root, boundary) { + cr.ui.FocusRow.call(this, root, boundary); -/** - * Decorates |rowElement| so that it can be treated as a HistoryFocusRow item. - * @param {Element} rowElement The element representing this row. - * @param {Node} boundary Focus events are ignored outside of this node. - */ -HistoryFocusRow.decorate = function(rowElement, boundary) { - rowElement.__proto__ = HistoryFocusRow.prototype; - rowElement.decorate(boundary); - - rowElement.addElementIfPresent_('.entry-box input', 'checkbox'); - rowElement.addElementIfPresent_('.domain-checkbox', 'checkbox'); - rowElement.addElementIfPresent_('.bookmark-section.starred', 'star'); - rowElement.addElementIfPresent_('[is="action-link"]', 'domain'); - rowElement.addElementIfPresent_('.title a', 'title'); - rowElement.addElementIfPresent_('.drop-down', 'menu'); -}; + // None of these are guaranteed to exist in all versions of the UI. + this.addItem('checkbox', '.entry-box input'); + this.addItem('checkbox', '.domain-checkbox'); + this.addItem('star', '.bookmark-section.starred'); + this.addItem('domain', '[is="action-link"]'); + this.addItem('title', '.title a'); + this.addItem('menu', '.drop-down'); +} HistoryFocusRow.prototype = { __proto__: cr.ui.FocusRow.prototype, /** @override */ - getEquivalentElement: function(element) { - if (this.contains(element)) - return element; - - // All elements default to another element with the same type. - var equivalent = this.getColumn_(element.getAttribute('focus-type')); - - if (!equivalent) { - switch (element.getAttribute('focus-type')) { - case 'star': - equivalent = this.getColumn_('title') || this.getColumn_('domain'); - break; - case 'domain': - equivalent = this.getColumn_('title'); - break; - case 'title': - equivalent = this.getColumn_('domain'); - break; - case 'menu': - return this.focusableElements[this.focusableElements.length - 1]; - } + getCustomEquivalent: function(sampleElement) { + var equivalent; + + switch (this.getTypeForElement(sampleElement)) { + case 'star': + equivalent = this.getFirstFocusable('title') || + this.getFirstFocusable('domain'); + break; + case 'domain': + equivalent = this.getFirstFocusable('title'); + break; + case 'title': + equivalent = this.getFirstFocusable('domain'); + break; + case 'menu': + equivalent = this.getFocusableElements().slice(-1)[0]; + break; } - return equivalent || this.focusableElements[0]; - }, - - /** - * @param {string} type The type of column to return. - * @return {?Element} The column matching the type. - * @private - */ - getColumn_: function(type) { - return this.querySelector('[focus-type=' + type + ']'); - }, - - /** - * @param {string} query A query to select the appropriate element. - * @param {string} type The type to use for the element. - * @private - */ - addElementIfPresent_: function(query, type) { - var element = this.querySelector(query); - if (element) { - this.addFocusableElement(element); - element.setAttribute('focus-type', type); - } + return equivalent || + cr.ui.FocusRow.prototype.getCustomEquivalent.call(this, sampleElement); }, }; @@ -1233,7 +1203,7 @@ HistoryView.prototype.onBeforeUnstarred = function(visit) { // Focus the title or domain when the bookmarked star is removed because the // star will no longer be focusable. - row.querySelector('[focus-type=title], [focus-type=domain]').focus(); + row.root.querySelector('[focus-type=title], [focus-type=domain]').focus(); }; /** @param {Visit} visit The visit that was just unstarred. */ @@ -1700,8 +1670,7 @@ HistoryView.prototype.updateFocusGrid_ = function() { for (var i = 0; i < rows.length; ++i) { assert(rows[i].parentNode); - HistoryFocusRow.decorate(rows[i], this.resultDiv_); - this.focusGrid_.addRow(rows[i]); + this.focusGrid_.addRow(new HistoryFocusRow(rows[i], this.resultDiv_)); } this.focusGrid_.ensureRowActive(); }; @@ -1797,14 +1766,11 @@ HistoryView.prototype.toggleGroupedVisits_ = function(e) { entry.classList.toggle('expand'); - var row = entry.querySelector('.site-domain-wrapper'); - var activeRows = - this.resultDiv_.getElementsByClassName(cr.ui.FocusRow.ACTIVE_CLASS); - for (var i = 0; i < activeRows.length; ++i) { - if (activeRows[i] != row) // Ignore |row| to avoid flicker. - activeRows[i].makeActive(false); - } - row.makeActive(true); + var root = entry.querySelector('.site-domain-wrapper'); + + this.focusGrid_.rows.forEach(function(row) { + row.makeActive(row.root == root); + }); this.updateFocusGrid_(); }; diff --git a/chrome/browser/resources/history/other_devices.js b/chrome/browser/resources/history/other_devices.js index 270f756..15d7d47 100644 --- a/chrome/browser/resources/history/other_devices.js +++ b/chrome/browser/resources/history/other_devices.js @@ -282,7 +282,9 @@ Device.prototype.createSessionContents_ = function(maxNumTabs) { a.addEventListener('click', makeClickHandler(sessionTag, String(win.sessionId), String(tab.sessionId))); - contents.appendChild(a); + var wrapper = createElementWithClassName('div', 'device-tab-wrapper'); + wrapper.appendChild(a); + contents.appendChild(wrapper); } else { numTabsHidden++; } @@ -295,7 +297,9 @@ Device.prototype.createSessionContents_ = function(maxNumTabs) { moreLink.addEventListener('click', this.view_.increaseRowHeight.bind( this.view_, this.row_, numTabsHidden)); moreLink.textContent = loadTimeData.getStringF('xMore', numTabsHidden); - contents.appendChild(moreLink); + var moreWrapper = createElementWithClassName('div', 'more-wrapper'); + moreWrapper.appendChild(moreLink); + contents.appendChild(moreWrapper); } return contents; @@ -422,31 +426,19 @@ DevicesView.prototype.increaseRowHeight = function(row, height) { // DevicesView, Private ------------------------------------------------------- /** - * Provides an implementation for a single column grid. + * @param {!Element} root + * @param {?Node} boundary * @constructor * @extends {cr.ui.FocusRow} */ -function DevicesViewFocusRow() {} - -/** - * Decorates |rowElement| so that it can be treated as a DevicesViewFocusRow. - * @param {Element} rowElement The element representing this row. - * @param {Node} boundary Focus events are ignored outside of this node. - */ -DevicesViewFocusRow.decorate = function(rowElement, boundary) { - rowElement.__proto__ = DevicesViewFocusRow.prototype; - rowElement.decorate(boundary); - rowElement.addFocusableElement(rowElement); -}; - -DevicesViewFocusRow.prototype = { - __proto__: cr.ui.FocusRow.prototype, +function DevicesViewFocusRow(root, boundary) { + cr.ui.FocusRow.call(this, root, boundary); + assert(this.addItem('menu-button', 'button.drop-down') || + this.addItem('device-tab', '.device-tab-entry') || + this.addItem('more-tabs', '.device-show-more-tabs')); +} - /** @override */ - getEquivalentElement: function(element) { - return this; - }, -}; +DevicesViewFocusRow.prototype = {__proto__: cr.ui.FocusRow.prototype}; /** * Update the page with results. @@ -503,14 +495,14 @@ DevicesView.prototype.displayResults_ = function() { var devices = this.resultDiv_.querySelectorAll('.device-contents'); for (var i = 0; i < devices.length; ++i) { - var rows = devices[i].querySelectorAll('.device-tab-entry, button'); + var rows = devices[i].querySelectorAll( + 'h3, .device-tab-wrapper, .more-wrapper'); if (!rows.length) continue; var grid = new cr.ui.FocusGrid(); for (var j = 0; j < rows.length; ++j) { - DevicesViewFocusRow.decorate(rows[j], devices[i]); - grid.addRow(rows[j]); + grid.addRow(new DevicesViewFocusRow(rows[j], devices[i])); } grid.ensureRowActive(); this.focusGrids_.push(grid); diff --git a/ui/webui/resources/js/cr/ui/focus_grid.js b/ui/webui/resources/js/cr/ui/focus_grid.js index f6f80d0..3fc2dbf 100644 --- a/ui/webui/resources/js/cr/ui/focus_grid.js +++ b/ui/webui/resources/js/cr/ui/focus_grid.js @@ -64,7 +64,7 @@ cr.define('cr.ui', function() { // Only the clicked row should be active. var target = assertInstanceof(e.target, Node); this.focusGrid_.rows.forEach(function(row) { - row.makeActive(row.contains(target)); + row.makeActive(row.root.contains(target)); }); return true; @@ -86,13 +86,25 @@ cr.define('cr.ui', function() { */ getRowIndexForTarget: function(target) { for (var i = 0; i < this.rows.length; ++i) { - if (this.rows[i].focusableElements.indexOf(target) >= 0) + if (this.rows[i].getElements().indexOf(target) >= 0) return i; } return -1; }, /** + * @param {Element} root An element to search for. + * @return {?cr.ui.FocusRow} The row with root of |root| or null. + */ + getRowForRoot: function(root) { + for (var i = 0; i < this.rows.length; ++i) { + if (this.rows[i].root == root) + return this.rows[i]; + } + return null; + }, + + /** * Handles keyboard shortcuts to move up/down in the grid. * @param {Event} e The key event. * @private diff --git a/ui/webui/resources/js/cr/ui/focus_row.js b/ui/webui/resources/js/cr/ui/focus_row.js index b117266..d520e62 100644 --- a/ui/webui/resources/js/cr/ui/focus_row.js +++ b/ui/webui/resources/js/cr/ui/focus_row.js @@ -5,19 +5,6 @@ cr.define('cr.ui', function() { /** * A class to manage focus between given horizontally arranged elements. - * For example, given the page: - * - * <input type="checkbox"> <label>Check me!</label> <button>X</button> - * - * One could create a FocusRow by doing: - * - * var focusRow = new cr.ui.FocusRow(rowBoundary, rowEl); - * - * focusRow.addFocusableElement(checkboxEl); - * focusRow.addFocusableElement(labelEl); - * focusRow.addFocusableElement(buttonEl); - * - * focusRow.setInitialFocusability(true); * * Pressing left cycles backward and pressing right cycles forward in item * order. Pressing Home goes to the beginning of the list and End goes to the @@ -28,18 +15,35 @@ cr.define('cr.ui', function() { * changes to a node inside |this.boundary_|. If |boundary| isn't specified, * any focus change deactivates the row. * + * @param {!Element} root The root of this focus row. Focus classes are + * applied to |root| and all added elements must live within |root|. + * @param {?Node} boundary Focus events are ignored outside of this node. + * @param {cr.ui.FocusRow.Delegate=} opt_delegate An optional event delegate. * @constructor - * @extends {HTMLElement} */ - function FocusRow() {} + function FocusRow(root, boundary, opt_delegate) { + /** @type {!Element} */ + this.root = root; + + /** @private {!Node} */ + this.boundary_ = boundary || document; + + /** @type {cr.ui.FocusRow.Delegate|undefined} */ + this.delegate = opt_delegate; + + /** @private {!EventTracker} */ + this.eventTracker_ = new EventTracker; + this.eventTracker_.add(cr.doc, 'focusin', this.onFocusin_.bind(this)); + this.eventTracker_.add(cr.doc, 'keydown', this.onKeydown_.bind(this)); + } /** @interface */ FocusRow.Delegate = function() {}; FocusRow.Delegate.prototype = { /** - * Called when a key is pressed while an item in |this.focusableElements| is - * focused. If |e|'s default is prevented, further processing is skipped. + * Called when a key is pressed while on a typed element. If |e|'s default + * is prevented, further processing is skipped. * @param {cr.ui.FocusRow} row The row that detected a keydown. * @param {Event} e * @return {boolean} Whether the event was handled. @@ -62,84 +66,132 @@ cr.define('cr.ui', function() { * @param {Element} element * @return {boolean} Whether the item is focusable. */ - FocusRow.isFocusable = function isFocusable(element) { - if (!element) + FocusRow.isFocusable = function(element) { + if (!element || element.disabled) return false; - // Hidden elements are not focusable. - var style = window.getComputedStyle(element); - if (style.visibility == 'hidden' || style.display == 'none') - return false; + // We don't check that element.tabIndex >= 0 here because inactive rows set + // a tabIndex of -1. + + function isVisible(element) { + var style = window.getComputedStyle(element); + if (style.visibility == 'hidden' || style.display == 'none') + return false; + + if (element.parentNode == element.ownerDocument) + return true; + + return isVisible(element.parentElement); + } - // Verify all ancestors are focusable. - return !element.parentElement || isFocusable(element.parentElement); + return isVisible(element); }; FocusRow.prototype = { - __proto__: HTMLElement.prototype, - /** - * Should be called in the constructor to decorate |this|. - * @param {Node} boundary Focus events are ignored outside of this node. - * @param {cr.ui.FocusRow.Delegate=} opt_delegate A delegate to handle key - * events. + * Register a new type of focusable element (or add to an existing one). + * + * Example: an (X) button might be 'delete' or 'close'. + * + * When FocusRow is used within a FocusGrid, these types are used to + * determine equivalent controls when Up/Down are pressed to change rows. + * + * Another example: mutually exclusive controls that hide eachother on + * activation (i.e. Play/Pause) could use the same type (i.e. 'play-pause') + * to indicate they're equivalent. + * + * @param {string} type The type of element to track focus of. + * @param {string} query The selector of the element from this row's root. + * @return {boolean} Whether a new item was added. */ - decorate: function(boundary, opt_delegate) { - /** @private {!Node} */ - this.boundary_ = boundary || document; + addItem: function(type, query) { + assert(type); - /** @type {cr.ui.FocusRow.Delegate|undefined} */ - this.delegate = opt_delegate; + var element = this.root.querySelector(query); + if (!element) + return false; - /** @type {Array<Element>} */ - this.focusableElements = []; + element.setAttribute('focus-type', type); + element.tabIndex = this.isActive() ? 0 : -1; - /** @private {!EventTracker} */ - this.eventTracker_ = new EventTracker; - this.eventTracker_.add(cr.doc, 'focusin', this.onFocusin_.bind(this)); - this.eventTracker_.add(cr.doc, 'keydown', this.onKeydown_.bind(this)); + this.eventTracker_.add(element, 'mousedown', + this.onMousedown_.bind(this)); + return true; + }, + + /** Dereferences nodes and removes event handlers. */ + destroy: function() { + this.eventTracker_.removeAll(); + }, + + /** + * @param {Element} sampleElement An element for to find an equivalent for. + * @return {!Element} An equivalent element to focus for |sampleElement|. + * @protected + */ + getCustomEquivalent: function(sampleElement) { + return assert(this.getFirstFocusable()); + }, + + /** + * @return {!Array<!Element>} All registered elements (regardless of + * focusability). + */ + getElements: function() { + var elements = this.root.querySelectorAll('[focus-type]'); + return Array.prototype.slice.call(elements); }, /** * Find the element that best matches |sampleElement|. - * @param {Element} sampleElement An element from a row of the same type + * @param {!Element} sampleElement An element from a row of the same type * which previously held focus. * @return {!Element} The element that best matches sampleElement. */ - getEquivalentElement: function(sampleElement) { assertNotReached(); }, + getEquivalentElement: function(sampleElement) { + if (this.getFocusableElements().indexOf(sampleElement) >= 0) + return sampleElement; + + var sampleFocusType = this.getTypeForElement(sampleElement); + if (sampleFocusType) { + var sameType = this.getFirstFocusable(sampleFocusType); + if (sameType) + return sameType; + } + + return this.getCustomEquivalent(sampleElement); + }, /** - * Add an element to this FocusRow. No-op if |element| is not provided. - * @param {Element} element The element that should be added. + * @param {string=} opt_type An optional type to search for. + * @return {?Element} The first focusable element with |type|. */ - addFocusableElement: function(element) { - if (!element) - return; - - assert(this.focusableElements.indexOf(element) == -1); - assert(this.contains(element)); - - element.tabIndex = this.isActive() ? 0 : -1; + getFirstFocusable: function(opt_type) { + var filter = opt_type ? '="' + opt_type + '"' : ''; + var elements = this.root.querySelectorAll('[focus-type' + filter + ']'); + for (var i = 0; i < elements.length; ++i) { + if (cr.ui.FocusRow.isFocusable(elements[i])) + return elements[i]; + } + return null; + }, - this.focusableElements.push(element); - this.eventTracker_.add(element, 'mousedown', - this.onMousedown_.bind(this)); + /** @return {!Array<!Element>} Registered, focusable elements. */ + getFocusableElements: function() { + return this.getElements().filter(cr.ui.FocusRow.isFocusable); }, /** - * Called when focus changes to activate/deactivate the row. Focus is - * removed from the row when |element| is not in the FocusRow. - * @param {Element} element The element that has focus. null if focus should - * be removed. - * @private - */ - onFocusChange_: function(element) { - this.makeActive(this.contains(element)); + * @param {!Element} element An element to determine a focus type for. + * @return {string} The focus type for |element| or '' if none. + */ + getTypeForElement: function(element) { + return element.getAttribute('focus-type') || ''; }, /** @return {boolean} Whether this row is currently active. */ isActive: function() { - return this.classList.contains(FocusRow.ACTIVE_CLASS); + return this.root.classList.contains(FocusRow.ACTIVE_CLASS); }, /** @@ -151,17 +203,22 @@ cr.define('cr.ui', function() { if (active == this.isActive()) return; - this.focusableElements.forEach(function(element) { + this.getElements().forEach(function(element) { element.tabIndex = active ? 0 : -1; }); - this.classList.toggle(FocusRow.ACTIVE_CLASS, active); + this.root.classList.toggle(FocusRow.ACTIVE_CLASS, active); }, - /** Dereferences nodes and removes event handlers. */ - destroy: function() { - this.focusableElements.length = 0; - this.eventTracker_.removeAll(); + /** + * Called when focus changes to activate/deactivate the row. Focus is + * removed from the row when |element| is not in the FocusRow. + * @param {Element} element The element that has focus. null if focus should + * be removed. + * @private + */ + onFocusChange_: function(element) { + this.makeActive(this.root.contains(element)); }, /** @@ -181,7 +238,9 @@ cr.define('cr.ui', function() { */ onKeydown_: function(e) { var element = assertInstanceof(e.target, Element); - var elementIndex = this.focusableElements.indexOf(element); + + var elements = this.getFocusableElements(); + var elementIndex = elements.indexOf(element); if (elementIndex < 0) return; @@ -200,9 +259,9 @@ cr.define('cr.ui', function() { else if (e.keyIdentifier == 'Home') index = 0; else if (e.keyIdentifier == 'End') - index = this.focusableElements.length - 1; + index = elements.length - 1; - var elementToFocus = this.focusableElements[index]; + var elementToFocus = elements[index]; if (elementToFocus) { this.getEquivalentElement(elementToFocus).focus(); e.preventDefault(); |
