blob: 48d0c10ce0948eaa15265c7a74e597bf098bbbbf [file] [log] [blame]
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of app;
abstract class TableTreeRow extends Observable {
static const arrowRight = '\u2192';
static const arrowDownRight = '\u21b3';
// Number of pixels each subtree is indented.
static const subtreeIndent = 16;
final TableTree tree;
final TableTreeRow parent;
final int depth;
final List<TableTreeRow> children = new List<TableTreeRow>();
final List<TableCellElement> tableColumns = new List<TableCellElement>();
SpanElement _expander;
TableRowElement _tr;
TableRowElement get tr {
assert(_tr != null);
return _tr;
}
TableTreeRow(this.tree, TableTreeRow parent) :
parent = parent,
depth = parent != null ? parent.depth+1 : 0 {
}
bool _expanded = false;
bool get expanded => _expanded;
set expanded(bool expanded) {
var changed = _expanded != expanded;
_expanded = expanded;
if (changed) {
// If the state has changed, fire callbacks.
if (_expanded) {
_onExpand();
} else {
_onCollapse();
}
}
}
bool expandOrCollapse() {
expanded = !expanded;
return expanded;
}
bool hasChildren();
String _backgroundColorClassForRow() {
const colors = const ['rowColor0', 'rowColor1', 'rowColor2', 'rowColor3',
'rowColor4', 'rowColor5', 'rowColor6', 'rowColor7',
'rowColor8'];
var index = (depth - 1) % colors.length;
return colors[index];
}
void _buildRow() {
_tr = new TableRowElement();
for (var i = 0; i < tree.columnCount; i++) {
var cell = _tr.insertCell(-1);
cell.classes.add(_backgroundColorClassForRow());
tableColumns.add(cell);
}
var firstColumn = tableColumns[0];
_expander = new SpanElement();
_expander.style.display = 'inline-block';
_expander.style.minWidth = '1.5em';
_expander.onClick.listen(onClick);
firstColumn.children.add(_expander);
firstColumn.style.paddingLeft = '${depth * subtreeIndent}px';
updateExpanderView();
}
void updateExpanderView() {
if (_expander == null) {
return;
}
if (!hasChildren()) {
_expander.style.visibility = 'hidden';
_expander.style.cursor = 'auto';
return;
} else {
_expander.style.visibility = 'visible';
_expander.style.cursor = 'pointer';
}
_expander.text = expanded ? arrowDownRight : arrowRight;
}
/// Fired when the tree row is being shown.
/// Populate tr and add logical children here.
void onShow() {
assert(_tr == null);
_buildRow();
}
/// Fired when the tree row is being hidden.
void onHide() {
assert(_tr != null);
_tr = null;
tableColumns.clear();
_expander = null;
}
/// Fired when the tree row is being expanded.
void _onExpand() {
for (var child in children) {
child.onShow();
child.updateExpanderView();
}
updateExpanderView();
}
/// Fired when the tree row is being collapsed.
void _onCollapse() {
for (var child in children) {
child.onHide();
}
updateExpanderView();
}
void onClick(Event e) {
tree.toggle(this);
e.stopPropagation();
}
}
class TableTree extends Observable {
final TableSectionElement tableBody;
final List<TableTreeRow> rows = [];
final int columnCount;
/// Create a table tree with column [headers].
TableTree(this.tableBody, this.columnCount);
/// Initialize the table tree with the list of root children.
void initialize(TableTreeRow root) {
tableBody.children.clear();
rows.clear();
root.onShow();
rows.addAll(root.children);
for (var i = 0; i < rows.length; i++) {
rows[i].onShow();
tableBody.children.add(rows[i].tr);
}
}
/// Toggle expansion of row in tree.
void toggle(TableTreeRow row) {
if (row.expandOrCollapse()) {
_expand(row);
} else {
_collapse(row);
}
}
int _index(TableTreeRow row) => rows.indexOf(row);
void _expand(TableTreeRow row) {
int index = _index(row);
assert(index != -1);
rows.insertAll(index + 1, row.children);
for (var i = 0; i < row.children.length; i++) {
tableBody.children.insert(index + i + 1, row.children[i].tr);
}
}
void _collapse(TableTreeRow row) {
var childCount = row.children.length;
if (childCount == 0) {
return;
}
for (var i = 0; i < childCount; i++) {
// Close all inner rows.
if (row.children[i].expanded) {
_collapse(row.children[i]);
}
}
// Collapse this row.
row.expanded = false;
// Remove all children.
int index = _index(row);
rows.removeRange(index + 1, index + 1 + childCount);
for (var i = 0; i < childCount; i++) {
tableBody.children.removeAt(index + 1);
}
}
}
typedef String ValueFormatter(dynamic value);
class SortedTableColumn {
static String toStringFormatter(dynamic v) {
return v != null ? v.toString() : '<null>';
}
final String label;
final ValueFormatter formatter;
SortedTableColumn.withFormatter(this.label, this.formatter);
SortedTableColumn(this.label)
: formatter = toStringFormatter;
}
class SortedTableRow {
final List values;
SortedTableRow(this.values);
}
class SortedTable extends Observable {
final List<SortedTableColumn> columns;
final List<SortedTableRow> rows = new List<SortedTableRow>();
final List<int> sortedRows = [];
SortedTable(this.columns);
int _sortColumnIndex = 0;
set sortColumnIndex(var index) {
assert(index >= 0);
assert(index < columns.length);
_sortColumnIndex = index;
notifyPropertyChange(#getColumnLabel, 0, 1);
}
int get sortColumnIndex => _sortColumnIndex;
bool _sortDescending = true;
bool get sortDescending => _sortDescending;
set sortDescending(var descending) {
_sortDescending = descending;
notifyPropertyChange(#getColumnLabel, 0, 1);
}
dynamic getSortKeyFor(int row, int col) {
return rows[row].values[col];
}
int _sortFuncDescending(int i, int j) {
var a = getSortKeyFor(i, _sortColumnIndex);
var b = getSortKeyFor(j, _sortColumnIndex);
return b.compareTo(a);
}
int _sortFuncAscending(int i, int j) {
var a = getSortKeyFor(i, _sortColumnIndex);
var b = getSortKeyFor(j, _sortColumnIndex);
return a.compareTo(b);
}
void sort() {
Stopwatch sw = new Stopwatch()..start();
assert(_sortColumnIndex >= 0);
assert(_sortColumnIndex < columns.length);
if (_sortDescending) {
sortedRows.sort(_sortFuncDescending);
} else {
sortedRows.sort(_sortFuncAscending);
}
}
void clearRows() {
rows.clear();
sortedRows.clear();
}
void addRow(SortedTableRow row) {
sortedRows.add(rows.length);
rows.add(row);
}
String getFormattedValue(int row, int column) {
var value = getValue(row, column);
var formatter = columns[column].formatter;
return formatter(value);
}
@observable String getColumnLabel(int column) {
assert(column >= 0);
assert(column < columns.length);
// TODO(johnmccutchan): Move expander display decisions into html once
// tables and templates are better supported.
const arrowUp = '\u25BC';
const arrowDown = '\u25B2';
if (column != _sortColumnIndex) {
return columns[column].label + '\u2003';
}
return columns[column].label + (_sortDescending ? arrowUp : arrowDown);
}
dynamic getValue(int row, int column) {
return rows[row].values[column];
}
}