blob: cbd12b2d39dd0b9963489104599e3cbd11fb935c [file] [log] [blame]
// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
lib.rtdep('lib.f');
/**
* UI Element that controls the multi-column list in the connect dialog.
*
* Maybe it should be promoted to a shared lib at some point.
*/
nassh.ColumnList = function(div, items, opt_columnCount) {
this.div_ = div || null;
this.items_ = items;
this.columnCount = opt_columnCount || 2;
this.activeIndex = null;
// List of callbacks to invoke after the next redraw().
this.afterRedraw_ = [];
this.document_ = null;
if (div)
this.decorate(div);
};
/**
* Turn a div into a ColumnList.
*/
nassh.ColumnList.prototype.decorate = function(div) {
this.div_ = div;
this.document_ = div.ownerDocument;
this.div_.style.overflowY = 'auto';
this.div_.style.overflowX = 'hidden';
this.div_.addEventListener('keydown', this.onKeyDown_.bind(this));
var baseId = this.div_.getAttribute('id');
if (!baseId) {
baseId = Math.floor(Math.random() * 0xffff + 1).toString(16);
baseId = lib.f.zpad(baseID, 4);
baseId = 'columnlist-' + baseID;
}
this.baseId_ = baseId;
this.redraw();
};
/**
* Focus the ColumnList.
*/
nassh.ColumnList.prototype.focus = function() {
if (!this.div_)
throw 'Not initialized.';
this.div_.focus();
};
/**
* Add an event listener.
*/
nassh.ColumnList.prototype.addEventListener = function(var_args) {
if (!this.div_)
throw 'Not initialized.';
this.div_.addEventListener.apply(this.div_, arguments);
};
/**
* Have the ColumnList redraw after a brief timeout.
*
* Coalesces multiple invocations during the timeout period.
*/
nassh.ColumnList.prototype.scheduleRedraw = function() {
if (this.redrawTimeout_)
return;
this.redrawTimeout_ = setTimeout(() => {
this.redrawTimeout_ = null;
this.redraw();
}, 100);
};
/**
* Emoty out and redraw the list.
*/
nassh.ColumnList.prototype.redraw = function() {
var div = this.div_;
while (div.firstChild) {
div.removeChild(div.firstChild);
}
div.setAttribute('tabindex', '0');
div.setAttribute('role', 'listbox');
if (!this.items_.length)
return;
var columnWidth = (1 / this.columnCount * 100) + '%';
var table = this.document_.createElement('table');
table.style.tableLayout = 'fixed';
table.style.width = '100%';
div.appendChild(table);
var tbody = this.document_.createElement('tbody');
table.appendChild(tbody);
var tr;
for (var i = 0; i < this.items_.length; i++) {
var row = Math.floor(i / this.columnCount);
var column = i % this.columnCount;
var td = this.document_.createElement('td');
td.setAttribute('role', 'option');
td.setAttribute('id', this.baseId_ + '-item-' + i);
td.setAttribute('row', row);
td.setAttribute('column', column);
td.style.width = columnWidth;
td.className = 'column-list-item';
var item = this.document_.createElement('div');
item.textContent = this.items_[i].textContent || 'no-name';
item.addEventListener('click', this.onItemClick_.bind(this, td));
item.addEventListener('dblclick', this.onItemClick_.bind(this, td));
td.appendChild(item);
if (column == 0) {
tr = this.document_.createElement('tr');
tbody.appendChild(tr);
}
tr.appendChild(td);
}
this.setActiveIndex(Math.min(this.activeIndex, this.items_.length - 1));
while (this.afterRedraw_.length) {
var callback = this.afterRedraw_.pop();
callback();
}
};
/**
* Function to invoke after the next redraw() happens.
*
* Use this if you're doing something that will cause a redraw (like modifying a
* preference linked to the list), and you have something to finish after the
* redraw.
*/
nassh.ColumnList.prototype.afterNextRedraw = function(callback) {
this.afterRedraw_.push(callback);
};
/**
* Set the index of the item that should be considered "active".
*/
nassh.ColumnList.prototype.setActiveIndex = function(i) {
if (isNaN(i))
throw new Error('Index is NaN');
var before = this.activeIndex;
if (i != this.activeIndex) {
var n = this.getActiveNode_();
if (n)
n.classList.remove('active');
setTimeout(
this.onActiveIndexChanged.bind(this, {before: before, now: i}), 0);
}
this.activeIndex = i;
var node = this.getActiveNode_();
node.classList.add('active');
this.div_.setAttribute('aria-activedescendant', node.getAttribute('id'));
setTimeout(node.scrollIntoViewIfNeeded.bind(node), 0);
};
/**
* Return the outer DOM node for the active item.
*/
nassh.ColumnList.prototype.getActiveNode_ = function() {
return this.getNodeByIndex_(this.activeIndex);
};
/**
* Given an index into the list, return the (row, column) location.
*/
nassh.ColumnList.prototype.getRowColByIndex_ = function(i) {
return {
row: parseInt(i / this.columnCount),
column: i % this.columnCount
};
};
/**
* Given a 1d index into the list, return the DOM node.
*/
nassh.ColumnList.prototype.getNodeByIndex_ = function(i) {
var rc = this.getRowColByIndex_(i);
return this.getNodeByRowCol_(rc.row, rc.column);
};
/**
* Given a (row, column) location, return an index into the list.
*/
nassh.ColumnList.prototype.getIndexByRowCol_ = function(
row, column) {
return this.columnCount * row + column;
};
/**
* Given a (row, column) location, return a DOM node.
*/
nassh.ColumnList.prototype.getNodeByRowCol_ = function(row, column) {
return this.div_.querySelector(
'[row="' + row + '"][column="' + column + '"]');
};
/**
* Someone clicked on an item in the list.
*/
nassh.ColumnList.prototype.onItemClick_ = function(srcNode, e) {
var i = this.getIndexByRowCol_(parseInt(srcNode.getAttribute('row')),
parseInt(srcNode.getAttribute('column')));
this.setActiveIndex(i);
e.preventDefault();
return false;
};
/**
* Return the height (in items) of a given, zero-based column.
*/
nassh.ColumnList.prototype.getColumnHeight_ = function(column) {
var tallestColumn = Math.ceil(this.items_.length / this.columnCount);
if (column + 1 <= Math.floor(this.columnCount / column + 1))
return tallestColumn;
return tallestColumn - 1;
};
/**
* Clients can override this to learn when the active index changes.
*/
nassh.ColumnList.prototype.onActiveIndexChanged = function(e) { };
/**
* Clients can override this to handle onKeyDown events.
*
* They can return false (literally) to block the ColumnList from also
* handling the event.
*/
nassh.ColumnList.prototype.onKeyDown = function(e) { };
/**
* Handle a key down event on the div.
*/
nassh.ColumnList.prototype.onKeyDown_ = function(e) {
if (this.onKeyDown(e) === false)
return;
var i = this.activeIndex;
var rc = this.getRowColByIndex_(i);
var node = this.getActiveNode_();
switch (e.keyCode) {
case 38: // UP
if (i == 0) {
// UP from the first item, warp to the last.
i = this.items_.length - 1;
} else if (rc.row == 0) {
// UP from the first row, warp to bottom of previous column.
i = this.getIndexByRowCol_(this.getColumnHeight_(rc.column - 1) - 1,
rc.column - 1);
} else {
// UP from anywhere else, just move up a row.
i = this.getIndexByRowCol_(rc.row - 1, rc.column);
}
break;
case 40: // DOWN
if (i == this.items_.length - 1) {
// DOWN from last item, warp to the first.
i = 0;
} else if (rc.row == this.getColumnHeight_(rc.column) - 1) {
// DOWN from the bottom row, warp to top of the next.
i = this.getIndexByRowCol_(0, rc.column + 1);
} else {
// DOWN from anywhere else, move down a row.
i = this.getIndexByRowCol_(rc.row + 1, rc.column);
}
break;
case 39: // RIGHT
if (i == this.items_.length - 1) {
// RIGHT from last item, warp to the first.
i = 0;
} else if (rc.column >= this.columnCount - 1 ||
rc.row >= this.getColumnHeight_(rc.column + 1)) {
// RIGHT from last column (of this row), warp to the first column of
// next row.
i = this.getIndexByRowCol_(rc.row + 1, 0);
} else {
// RIGHT from anywhere else, move right a column.
i = this.getIndexByRowCol_(rc.row, rc.column + 1);
}
break;
case 37: // LEFT
if (i == 0) {
// LEFT from first item, warp to the last.
i = this.items_.length - 1;
} else if (rc.column == 0) {
// LEFT from first column, warp to the last column of previous row.
i = this.getIndexByRowCol_(rc.row - 1, this.columnCount - 1);
} else {
// LEFT from anywhere else, move left a column.
i = this.getIndexByRowCol_(rc.row, rc.column - 1);
}
break;
}
if (i != this.activeIndex) {
this.setActiveIndex(i);
}
};