summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authordbeam <dbeam@chromium.org>2015-08-13 20:20:07 -0700
committerCommit bot <commit-bot@chromium.org>2015-08-14 03:20:42 +0000
commitf9d4799b5b9c3f5c5694e24d3500a4bbe75950e9 (patch)
tree1d4fb5bd4324e5b03f2be6bd6a0d174ecfda2a0f
parentf2a64e426b227a4c752be75034094b2a197288f1 (diff)
downloadchromium_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.js82
-rw-r--r--chrome/browser/resources/downloads/manager.js20
-rw-r--r--chrome/browser/resources/extensions/compiled_resources.gyp1
-rw-r--r--chrome/browser/resources/extensions/extension_error.js71
-rw-r--r--chrome/browser/resources/extensions/extension_list.js608
-rw-r--r--chrome/browser/resources/extensions/extensions.html2
-rw-r--r--chrome/browser/resources/extensions/extensions.js2
-rw-r--r--chrome/browser/resources/extensions/focus_row.js30
-rw-r--r--chrome/browser/resources/history/history.js110
-rw-r--r--chrome/browser/resources/history/other_devices.js44
-rw-r--r--ui/webui/resources/js/cr/ui/focus_grid.js16
-rw-r--r--ui/webui/resources/js/cr/ui/focus_row.js209
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();