| // 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); |
| } |
| }; |