| // Copyright 2014 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('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: |
| * |
| * new cr.ui.FocusRow([checkboxEl, labelEl, buttonEl]) |
| * |
| * if there are references to each node or querying them from the DOM like so: |
| * |
| * new cr.ui.FocusRow(dialog.querySelectorAll('list input[type=checkbox]')) |
| * |
| * 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 |
| * end of the list. |
| * |
| * If an item in this row is focused, it'll stay active (accessible via tab). |
| * If no items in this row are focused, the row can stay active until focus |
| * changes to a node inside |this.boundary_|. If opt_boundary isn't |
| * specified, any focus change deactivates the row. |
| * |
| * @param {!Array.<!Element>|!NodeList} items Elements to track focus of. |
| * @param {Node=} opt_boundary Focus events are ignored outside of this node. |
| * @param {FocusRow.Delegate=} opt_delegate A delegate to handle key events. |
| * @param {FocusRow.Observer=} opt_observer An observer that's notified if |
| * this focus row is added to or removed from the focus order. |
| * @constructor |
| */ |
| function FocusRow(items, opt_boundary, opt_delegate, opt_observer) { |
| /** @type {!Array.<!Element>} */ |
| this.items = Array.prototype.slice.call(items); |
| assert(this.items.length > 0); |
| |
| /** @type {!Node} */ |
| this.boundary_ = opt_boundary || document; |
| |
| /** @private {cr.ui.FocusRow.Delegate|undefined} */ |
| this.delegate_ = opt_delegate; |
| |
| /** @private {cr.ui.FocusRow.Observer|undefined} */ |
| this.observer_ = opt_observer; |
| |
| /** @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.items.forEach(function(item) { |
| if (item != document.activeElement) |
| item.tabIndex = -1; |
| |
| this.eventTracker_.add(item, 'mousedown', this.onMousedown_.bind(this)); |
| }, this); |
| |
| /** |
| * The index that should be actively participating in the page tab order. |
| * @type {number} |
| * @private |
| */ |
| this.activeIndex_ = this.items.indexOf(document.activeElement); |
| } |
| |
| /** @interface */ |
| FocusRow.Delegate = function() {}; |
| |
| FocusRow.Delegate.prototype = { |
| /** |
| * Called when a key is pressed while an item in |this.items| is focused. 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. |
| */ |
| onKeydown: assertNotReached, |
| |
| /** |
| * @param {cr.ui.FocusRow} row The row that detected the mouse going down. |
| * @param {Event} e |
| * @return {boolean} Whether the event was handled. |
| */ |
| onMousedown: assertNotReached, |
| }; |
| |
| /** @interface */ |
| FocusRow.Observer = function() {}; |
| |
| FocusRow.Observer.prototype = { |
| /** |
| * Called when the row is activated (added to the focus order). |
| * @param {cr.ui.FocusRow} row The row added to the focus order. |
| */ |
| onActivate: assertNotReached, |
| |
| /** |
| * Called when the row is deactivated (removed from the focus order). |
| * @param {cr.ui.FocusRow} row The row removed from the focus order. |
| */ |
| onDeactivate: assertNotReached, |
| }; |
| |
| FocusRow.prototype = { |
| get activeIndex() { |
| return this.activeIndex_; |
| }, |
| set activeIndex(index) { |
| var wasActive = this.items[this.activeIndex_]; |
| if (wasActive) |
| wasActive.tabIndex = -1; |
| |
| this.items.forEach(function(item) { assert(item.tabIndex == -1); }); |
| this.activeIndex_ = index; |
| |
| if (this.items[index]) |
| this.items[index].tabIndex = 0; |
| |
| if (!this.observer_) |
| return; |
| |
| var isActive = index >= 0 && index < this.items.length; |
| if (isActive == !!wasActive) |
| return; |
| |
| if (isActive) |
| this.observer_.onActivate(this); |
| else |
| this.observer_.onDeactivate(this); |
| }, |
| |
| /** |
| * Focuses the item at |index|. |
| * @param {number} index An index to focus. Must be between 0 and |
| * this.items.length - 1. |
| */ |
| focusIndex: function(index) { |
| this.items[index].focus(); |
| }, |
| |
| /** Call this to clean up event handling before dereferencing. */ |
| destroy: function() { |
| this.eventTracker_.removeAll(); |
| }, |
| |
| /** |
| * @param {Event} e The focusin event. |
| * @private |
| */ |
| onFocusin_: function(e) { |
| if (this.boundary_.contains(assertInstanceof(e.target, Node))) |
| this.activeIndex = this.items.indexOf(e.target); |
| }, |
| |
| /** |
| * @param {Event} e A focus event. |
| * @private |
| */ |
| onKeydown_: function(e) { |
| var item = this.items.indexOf(e.target); |
| if (item < 0) |
| return; |
| |
| if (this.delegate_ && this.delegate_.onKeydown(this, e)) |
| return; |
| |
| var index = -1; |
| |
| if (e.keyIdentifier == 'Left') |
| index = item + (isRTL() ? 1 : -1); |
| else if (e.keyIdentifier == 'Right') |
| index = item + (isRTL() ? -1 : 1); |
| else if (e.keyIdentifier == 'Home') |
| index = 0; |
| else if (e.keyIdentifier == 'End') |
| index = this.items.length - 1; |
| |
| if (!this.items[index]) |
| return; |
| |
| this.focusIndex(index); |
| e.preventDefault(); |
| }, |
| |
| /** |
| * @param {Event} e A click event. |
| * @private |
| */ |
| onMousedown_: function(e) { |
| if (this.delegate_ && this.delegate_.onMousedown(this, e)) |
| return; |
| |
| if (!e.button) |
| this.activeIndex = this.items.indexOf(e.currentTarget); |
| }, |
| }; |
| |
| return { |
| FocusRow: FocusRow, |
| }; |
| }); |