blob: f098a4bc9a26a52f22537b07591b7d2d18de3d86 [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;
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.overflow = 'auto';
this.div_.addEventListener('keydown', this.onKeyDown_.bind(this));
this.redraw();
};
/**
* Focus the ColumnList.
*/
nassh.ColumnList.prototype.focus = function() {
if (!this.div_)
throw 'Not intialized.';
this.div_.focus();
};
/**
* Add an event listener.
*/
nassh.ColumnList.prototype.addEventListener = function(var_args) {
if (!this.div_)
throw 'Not intialized.';
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(function() {
this.redrawTimeout_ = null;
this.redraw();
}.bind(this), 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');
var baseID = div.getAttribute('id');
if (!baseID) {
baseID = Math.floor(Math.random() * 0xffff + 1).toString(16);
baseID = lib.f.zpad(baseID, 4);
baseID = 'columnlist-' + baseID;
}
if (!this.items_.length)
return;
for (var i = 0; i < this.columnCount; i++) {
var column = this.document_.createElement('div');
column.className = 'column-list-column';
column.setAttribute('x-vbox', 'x-vbox');
column.setAttribute('x-flex', '1');
div.appendChild(column);
}
for (var i = 0; i < this.items_.length; i++) {
var box = this.document_.createElement('div');
box.setAttribute('x-box', 'x-box');
box.setAttribute('role', 'option');
box.setAttribute('id', baseID + '-item-' + i);
box.className = 'column-list-item';
var item = this.document_.createElement('span');
item.textContent = this.items_[i].textContent || 'no-name';
item.addEventListener('click', this.onItemClick_.bind(this, box));
item.addEventListener('dblclick', this.onItemClick_.bind(this, box));
box.appendChild(item);
this.div_.childNodes[i % this.columnCount].appendChild(box);
}
this.setActiveIndex(Math.min(this.activeIndex, this.items_.length - 1));
};
/**
* 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) {
if (this.div_.childNodes.length <= column)
return null;
return this.div_.childNodes[column].childNodes[row] || null;
};
/**
* Someone clicked on an item in the list.
*/
nassh.ColumnList.prototype.onItemClick_ = function(srcNode, e) {
var node = srcNode.previousSibling;
var row = 0;
while (node) {
row++;
node = node.previousSibling;
}
node = srcNode.parentNode.previousSibling;
var column = 0;
while (node) {
column++;
node = node.previousSibling;
}
var i = this.getIndexByRowCol_(row, column);
this.setActiveIndex(i);
e.preventDefault();
return false;
};
/**
* Return the height (in items) of a given column.
*/
nassh.ColumnList.prototype.getColumnHeight_ = function(column) {
return this.div_.childNodes[column].childNodes.length;
};
/**
* 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);
}
};