blob: 7b86854d069d892fa5be08a04ffca94eee84c284 [file] [log] [blame]
// 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 grid of focusable elements in a 2D grid. For example,
* given this grid:
*
* focusable [focused] focusable (row: 0, col: 1)
* focusable focusable focusable
* focusable focusable focusable
*
* Pressing the down arrow would result in the focus moving down 1 row and
* keeping the same column:
*
* focusable focusable focusable
* focusable [focused] focusable (row: 1, col: 1)
* focusable focusable focusable
*
* And pressing right or tab at this point would move the focus to:
*
* focusable focusable focusable
* focusable focusable [focused] (row: 1, col: 2)
* focusable focusable focusable
*
* @constructor
* @implements {cr.ui.FocusRowDelegate}
*/
function FocusGrid() {
/** @type {!Array<!cr.ui.FocusRow>} */
this.rows = [];
}
FocusGrid.prototype = {
/** @private {boolean} */
ignoreFocusChange_: false,
/** @override */
onFocus: function(row, e) {
if (this.ignoreFocusChange_) {
this.ignoreFocusChange_ = false;
} else {
this.lastFocused_ = e.currentTarget;
}
this.rows.forEach(function(r) {
r.makeActive(r == row);
});
},
/** @override */
onKeydown: function(row, e) {
const rowIndex = this.rows.indexOf(row);
assert(rowIndex >= 0);
let newRow = -1;
if (e.key == 'ArrowUp') {
newRow = rowIndex - 1;
} else if (e.key == 'ArrowDown') {
newRow = rowIndex + 1;
} else if (e.key == 'PageUp') {
newRow = 0;
} else if (e.key == 'PageDown') {
newRow = this.rows.length - 1;
}
const rowToFocus = this.rows[newRow];
if (rowToFocus) {
this.ignoreFocusChange_ = true;
rowToFocus.getEquivalentElement(this.lastFocused_).focus();
e.preventDefault();
return true;
}
return false;
},
/** @override */
getCustomEquivalent: function(sampleElement) {
return null;
},
/**
* Unregisters event handlers and removes all |this.rows|.
*/
destroy: function() {
this.rows.forEach(function(row) {
row.destroy();
});
this.rows.length = 0;
},
/**
* @param {!Element} target A target item to find in this grid.
* @return {number} The row index. -1 if not found.
*/
getRowIndexForTarget: function(target) {
for (let i = 0; i < this.rows.length; ++i) {
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 (let i = 0; i < this.rows.length; ++i) {
if (this.rows[i].root == root) {
return this.rows[i];
}
}
return null;
},
/**
* Adds |row| to the end of this list.
* @param {!cr.ui.FocusRow} row The row that needs to be added to this grid.
*/
addRow: function(row) {
this.addRowBefore(row, null);
},
/**
* Adds |row| before |nextRow|. If |nextRow| is not in the list or it's
* null, |row| is added to the end.
* @param {!cr.ui.FocusRow} row The row that needs to be added to this grid.
* @param {cr.ui.FocusRow} nextRow The row that should follow |row|.
*/
addRowBefore: function(row, nextRow) {
row.delegate = row.delegate || this;
const nextRowIndex = nextRow ? this.rows.indexOf(nextRow) : -1;
if (nextRowIndex == -1) {
this.rows.push(row);
} else {
this.rows.splice(nextRowIndex, 0, row);
}
},
/**
* Removes a row from the focus row. No-op if row is not in the grid.
* @param {cr.ui.FocusRow} row The row that needs to be removed.
*/
removeRow: function(row) {
const nextRowIndex = row ? this.rows.indexOf(row) : -1;
if (nextRowIndex > -1) {
this.rows.splice(nextRowIndex, 1);
}
},
/**
* Makes sure that at least one row is active. Should be called once, after
* adding all rows to FocusGrid.
* @param {number=} preferredRow The row to select if no other row is
* active. Selects the first item if this is beyond the range of the
* grid.
*/
ensureRowActive: function(preferredRow) {
if (this.rows.length == 0) {
return;
}
for (let i = 0; i < this.rows.length; ++i) {
if (this.rows[i].isActive()) {
return;
}
}
(this.rows[preferredRow || 0] || this.rows[0]).makeActive(true);
},
};
return {
FocusGrid: FocusGrid,
};
});