blob: f73107d35e29c13b53106579fcdfe1d423d044d2 [file] [log] [blame]
<!DOCTYPE html>
<!--
Copyright (c) 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.
-->
<link rel="import" href="/tracing/ui/base/dom_helpers.html">
<link rel="import" href="/tracing/ui/base/utils.html">
<!--
@fileoverview A container that constructs a table-like container.
-->
<script>
'use strict';
tr.exportTo('tr.ui.b', function() {
var TableFormat = {};
TableFormat.SelectionMode = {
// Selection disabled.
// Default highlight: none.
NONE: 0,
// Row selection mode.
// Default highlight: dark row.
ROW: 1,
// Cell selection mode.
// Default highlight: dark cell and light row.
CELL: 2
};
TableFormat.HighlightStyle = {
// Highlight depends on the current selection mode.
DEFAULT: 0,
// No highlight.
NONE: 1,
// Light highlight.
LIGHT: 2,
// Dark highlight.
DARK: 3
};
TableFormat.ColumnAlignment = {
LEFT: 0 /* default */,
RIGHT: 1
};
return {
TableFormat: TableFormat
};
});
</script>
<dom-module id="tr-ui-b-table">
<template>
<style>
:host {
display: flex;
flex-direction: column;
}
table {
font-size: 12px;
flex: 1 1 auto;
align-self: stretch;
border-collapse: separate;
border-spacing: 0;
border-width: 0;
-webkit-user-select: initial;
}
tr > td {
padding: 2px 4px 2px 4px;
vertical-align: top;
}
tr:focus,
td:focus {
outline: 1px dotted rgba(0,0,0,0.1);
outline-offset: 0;
}
button.toggle-button {
height: 15px;
line-height: 60%;
vertical-align: middle;
width: 100%;
}
button > * {
height: 15px;
vertical-align: middle;
}
td.button-column {
width: 30px;
}
table > thead > tr > td.sensitive:hover {
background-color: #fcfcfc;
}
table > thead > tr > td {
font-weight: bold;
text-align: left;
background-color: #eee;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
border-top: 1px solid #ffffff;
border-bottom: 1px solid #aaa;
}
table > tfoot {
background-color: #eee;
font-weight: bold;
}
/* Light row and cell highlight. */
table > tbody[row-highlight-style="light"] > tr[selected],
table > tbody[cell-highlight-style="light"] > tr > td[selected] {
background-color: rgb(213, 236, 229); /* light turquoise */
}
table > tbody[row-highlight-style="light"] >
tr:not(.empty-row):not([selected]):hover,
table > tbody[cell-highlight-style="light"] >
tr:not(.empty-row):not([selected]) > td:hover {
background-color: #f6f6f6; /* light grey */
}
/* Dark row and cell highlight. */
table > tbody[row-highlight-style="dark"] > tr[selected],
table > tbody[cell-highlight-style="dark"] > tr > td[selected] {
background-color: rgb(103, 199, 165); /* turquoise */
}
table > tbody[row-highlight-style="dark"] >
tr:not(.empty-row):not([selected]):hover,
table > tbody[cell-highlight-style="dark"] >
tr:not(.empty-row):not([selected]) > td:hover {
background-color: #e6e6e6; /* grey */
}
table > tbody[row-highlight-style="dark"] > tr:hover[selected],
table > tbody[cell-highlight-style="dark"] > tr[selected] > td:hover {
background-color: rgb(171, 217, 202); /* semi-light turquoise */
}
table > colgroup > col[selected] {
background-color: #e6e6e6; /* grey */
}
table > tbody > tr.empty-row > td {
color: #666;
font-style: italic;
text-align: center;
}
table > tbody.has-footer > tr:last-child > td {
border-bottom: 1px solid #aaa;
}
table > tfoot > tr:first-child > td {
border-top: 1px solid #ffffff;
}
expand-button {
-webkit-user-select: none;
display: inline-block;
cursor: pointer;
font-size: 9px;
min-width: 8px;
max-width: 8px;
}
.button-expanded {
transform: rotate(90deg);
}
</style>
<table>
<colgroup id="cols">
</colgroup>
<thead id="head">
</thead>
<tbody id="body">
</tbody>
<tfoot id="foot">
</tfoot>
</table>
</template>
</dom-module>
<script>
'use strict';
(function() {
var RIGHT_ARROW = String.fromCharCode(0x25b6);
var UNSORTED_ARROW = String.fromCharCode(0x25BF);
var ASCENDING_ARROW = String.fromCharCode(0x25B4);
var DESCENDING_ARROW = String.fromCharCode(0x25BE);
var BASIC_INDENTATION = 8;
var SelectionMode = tr.ui.b.TableFormat.SelectionMode;
var HighlightStyle = tr.ui.b.TableFormat.HighlightStyle;
var ColumnAlignment = tr.ui.b.TableFormat.ColumnAlignment;
Polymer({
is: 'tr-ui-b-table',
created: function() {
this.selectionMode_ = SelectionMode.NONE;
this.rowHighlightStyle_ = HighlightStyle.DEFAULT;
this.cellHighlightStyle_ = HighlightStyle.DEFAULT;
this.selectedTableRowInfo_ = undefined;
this.selectedColumnIndex_ = undefined;
this.tableColumns_ = [];
this.tableRows_ = [];
this.tableRowsInfo_ = new WeakMap();
this.tableFooterRows_ = [];
this.tableFooterRowsInfo_ = new WeakMap();
this.sortColumnIndex_ = undefined;
this.sortDescending_ = false;
this.columnsWithExpandButtons_ = [];
this.headerCells_ = [];
this.showHeader_ = true;
this.emptyValue_ = undefined;
this.subRowsPropertyName_ = 'subRows';
this.customizeTableRowCallback_ = undefined;
this.defaultExpansionStateCallback_ = undefined;
this.userCanModifySortOrder_ = true;
},
ready: function() {
this.$.body.addEventListener(
'keydown', this.onKeyDown_.bind(this), true);
},
clear: function() {
this.selectionMode_ = SelectionMode.NONE;
this.rowHighlightStyle_ = HighlightStyle.DEFAULT;
this.cellHighlightStyle_ = HighlightStyle.DEFAULT;
this.selectedTableRowInfo_ = undefined;
this.selectedColumnIndex_ = undefined;
Polymer.dom(this).textContent = '';
this.tableColumns_ = [];
this.tableRows_ = [];
this.tableRowsInfo_ = new WeakMap();
this.tableFooterRows_ = [];
this.tableFooterRowsInfo_ = new WeakMap();
this.sortColumnIndex_ = undefined;
this.sortDescending_ = false;
this.columnsWithExpandButtons_ = [];
this.headerCells_ = [];
this.showHeader_ = true;
this.emptyValue_ = undefined;
this.subRowsPropertyName_ = 'subRows';
this.defaultExpansionStateCallback_ = undefined;
this.userCanModifySortOrder_ = true;
},
get showHeader() {
return this.showHeader_;
},
set showHeader(showHeader) {
this.showHeader_ = showHeader;
this.scheduleRebuildHeaders_();
},
set subRowsPropertyName(name) {
this.subRowsPropertyName_ = name;
},
/**
* This callback will be called whenever a body row is built
* for a userRow that has subRows and does not have an explicit
* isExpanded field.
* The callback should return true if the row should be expanded,
* or false if the row should be collapsed.
* @param {function(userRow, parentUserRow): boolean} cb The callback.
*/
set defaultExpansionStateCallback(cb) {
this.defaultExpansionStateCallback_ = cb;
this.scheduleRebuildBody_();
},
/**
* This callback will be called whenever a body row is built.
* The callback's return value is ignored.
* @param {function(userRow, trElement)} cb The callback.
*/
set customizeTableRowCallback(cb) {
this.customizeTableRowCallback_ = cb;
this.scheduleRebuildBody_();
},
get emptyValue() {
return this.emptyValue_;
},
set emptyValue(emptyValue) {
var previousEmptyValue = this.emptyValue_;
this.emptyValue_ = emptyValue;
if (this.tableRows_.length === 0 && emptyValue !== previousEmptyValue)
this.scheduleRebuildBody_();
},
/**
* Data objects should have the following fields:
* mandatory: title, value
* optional: width {string}, cmp {function}, colSpan {number},
* showExpandButtons {boolean},
* align {tr.ui.b.TableFormat.ColumnAlignment}
*
* @param {Array} columns An array of data objects.
*/
set tableColumns(columns) {
// Figure out the columns with expand buttons...
var columnsWithExpandButtons = [];
for (var i = 0; i < columns.length; i++) {
if (columns[i].showExpandButtons)
columnsWithExpandButtons.push(i);
}
if (columnsWithExpandButtons.length === 0) {
// First column if none have specified.
columnsWithExpandButtons = [0];
}
// Sanity check columns.
for (var i = 0; i < columns.length; i++) {
var colInfo = columns[i];
if (colInfo.width === undefined)
continue;
var hasExpandButton = columnsWithExpandButtons.indexOf(i) !== -1;
var w = colInfo.width;
if (w) {
if (/\d+px/.test(w)) {
continue;
} else if (/\d+%/.test(w)) {
if (hasExpandButton) {
throw new Error('Columns cannot be %-sized and host ' +
' an expand button');
}
} else {
throw new Error('Unrecognized width string');
}
}
}
// Commit the change.
this.tableColumns_ = columns;
this.headerCells_ = [];
this.columnsWithExpandButtons_ = columnsWithExpandButtons;
this.sortColumnIndex = undefined;
this.scheduleRebuildHeaders_();
// Blow away the table rows, too.
this.tableRows = this.tableRows_;
},
get tableColumns() {
return this.tableColumns_;
},
/**
* @param {Array} rows An array of 'row' objects with the following
* fields:
* optional: subRows An array of objects that have the same 'row'
* structure. Set subRowsPropertyName to use an
* alternative field name.
*/
set tableRows(rows) {
this.selectedTableRowInfo_ = undefined;
this.selectedColumnIndex_ = undefined;
this.maybeUpdateSelectedRow_();
this.tableRows_ = rows;
this.tableRowsInfo_ = new WeakMap();
this.scheduleRebuildBody_();
},
get tableRows() {
return this.tableRows_;
},
set footerRows(rows) {
this.tableFooterRows_ = rows;
this.tableFooterRowsInfo_ = new WeakMap();
this.scheduleRebuildFooter_();
},
get footerRows() {
return this.tableFooterRows_;
},
get userCanModifySortOrder() {
return this.userCanModifySortOrder_;
},
set userCanModifySortOrder(userCanModifySortOrder) {
var newUserCanModifySortOrder = !!userCanModifySortOrder;
if (newUserCanModifySortOrder === this.userCanModifySortOrder_)
return
this.userCanModifySortOrder_ = newUserCanModifySortOrder;
this.scheduleRebuildHeaders_();
},
set sortColumnIndex(number) {
if (number === this.sortColumnIndex_)
return;
if (number !== undefined) {
if (this.tableColumns_.length <= number)
throw new Error('Column number ' + number + ' is out of bounds.');
if (!this.tableColumns_[number].cmp)
throw new Error('Column ' + number + ' does not have a comparator.');
}
this.sortColumnIndex_ = number;
this.updateHeaderArrows_();
this.scheduleRebuildBody_();
this.dispatchSortingChangedEvent_();
},
get sortColumnIndex() {
return this.sortColumnIndex_;
},
set sortDescending(value) {
var newValue = !!value;
if (newValue !== this.sortDescending_) {
this.sortDescending_ = newValue;
this.updateHeaderArrows_();
this.scheduleRebuildBody_();
this.dispatchSortingChangedEvent_();
}
},
get sortDescending() {
return this.sortDescending_;
},
updateHeaderArrows_: function() {
for (var i = 0; i < this.headerCells_.length; i++) {
var headerCell = this.headerCells_[i];
var isColumnCurrentlySorted = i === this.sortColumnIndex_;
if (!this.tableColumns_[i].cmp ||
(!this.userCanModifySortOrder_ && !isColumnCurrentlySorted)) {
headerCell.sideContent = '';
continue;
}
if (!isColumnCurrentlySorted) {
headerCell.sideContent = UNSORTED_ARROW;
headerCell.sideContentDisabled = false;
continue;
}
headerCell.sideContent = this.sortDescending_ ?
DESCENDING_ARROW : ASCENDING_ARROW;
headerCell.sideContentDisabled = !this.userCanModifySortOrder_;
}
},
generateHeaderColumns_: function() {
var selectedTableColumnIndex = this.selectedTableColumnIndex;
Polymer.dom(this.$.cols).textContent = '';
for (var i = 0; i < this.tableColumns_.length; ++i) {
var colElement = document.createElement('col');
if (i === selectedTableColumnIndex) {
colElement.setAttribute('selected', true);
}
Polymer.dom(this.$.cols).appendChild(colElement);
}
this.headerCells_ = [];
Polymer.dom(this.$.head).textContent = '';
if (!this.showHeader_)
return;
var tr = this.appendNewElement_(this.$.head, 'tr');
for (var i = 0; i < this.tableColumns_.length; i++) {
var td = this.appendNewElement_(tr, 'td');
var headerCell = document.createElement('tr-ui-b-table-header-cell');
headerCell.column = this.tableColumns_[i];
// If the table can be sorted by this column and the user can modify
// the sort order, attach a tap callback to the column.
if (this.tableColumns_[i].cmp) {
var isColumnCurrentlySorted = i === this.sortColumnIndex_;
if (isColumnCurrentlySorted) {
headerCell.sideContent = this.sortDescending_ ?
DESCENDING_ARROW : ASCENDING_ARROW;
if (!this.userCanModifySortOrder_)
headerCell.sideContentDisabled = true;
}
if (this.userCanModifySortOrder_) {
Polymer.dom(td).classList.add('sensitive');
if (!isColumnCurrentlySorted)
headerCell.sideContent = UNSORTED_ARROW;
headerCell.tapCallback = this.createSortCallback_(i);
}
}
Polymer.dom(td).appendChild(headerCell);
this.headerCells_.push(headerCell);
}
},
applySizes_: function() {
if (this.tableRows_.length === 0 && !this.showHeader)
return;
var rowToRemoveSizing;
var rowToSize;
if (this.showHeader) {
rowToSize = Polymer.dom(this.$.head).children[0];
rowToRemoveSizing = Polymer.dom(this.$.body).children[0];
} else {
rowToSize = Polymer.dom(this.$.body).children[0];
rowToRemoveSizing = Polymer.dom(this.$.head).children[0];
}
for (var i = 0; i < this.tableColumns_.length; i++) {
if (rowToRemoveSizing && Polymer.dom(rowToRemoveSizing).children[i]) {
var tdToRemoveSizing = Polymer.dom(rowToRemoveSizing).children[i];
tdToRemoveSizing.style.minWidth = '';
tdToRemoveSizing.style.width = '';
}
// Apply sizing.
var td = Polymer.dom(rowToSize).children[i];
var delta;
if (this.columnsWithExpandButtons_.indexOf(i) !== -1) {
td.style.paddingLeft = BASIC_INDENTATION + 'px';
delta = BASIC_INDENTATION + 'px';
} else {
delta = undefined;
}
function calc(base, delta) {
if (delta)
return 'calc(' + base + ' - ' + delta + ')';
else
return base;
}
var w = this.tableColumns_[i].width;
if (w) {
if (/\d+px/.test(w)) {
td.style.minWidth = calc(w, delta);
} else if (/\d+%/.test(w)) {
td.style.width = w;
} else {
throw new Error('Unrecognized width string: ' + w);
}
}
}
},
createSortCallback_: function(columnNumber) {
return function() {
if (!this.userCanModifySortOrder_)
return;
var previousIndex = this.sortColumnIndex;
this.sortColumnIndex = columnNumber;
if (previousIndex !== columnNumber)
this.sortDescending = false;
else
this.sortDescending = !this.sortDescending;
}.bind(this);
},
generateTableRowNodes_: function(tableSection, userRows, rowInfoMap,
indentation, lastAddedRow,
parentRowInfo) {
if (this.sortColumnIndex_ !== undefined &&
tableSection === this.$.body) {
userRows = userRows.slice(); // Don't mess with the input data.
userRows.sort(function(rowA, rowB) {
var c = this.tableColumns_[this.sortColumnIndex_].cmp(
rowA, rowB);
if (this.sortDescending_)
c = -c;
return c;
}.bind(this));
}
for (var i = 0; i < userRows.length; i++) {
var userRow = userRows[i];
var rowInfo = this.getOrCreateRowInfoFor_(rowInfoMap, userRow,
parentRowInfo);
var htmlNode = this.getHTMLNodeForRowInfo_(
tableSection, rowInfo, rowInfoMap, indentation);
if (lastAddedRow === undefined) {
// Put first into the table.
Polymer.dom(tableSection).insertBefore(
htmlNode, Polymer.dom(tableSection).firstChild);
} else {
// This is shorthand for insertAfter(htmlNode, lastAdded).
var nextSiblingOfLastAdded = Polymer.dom(lastAddedRow).nextSibling;
Polymer.dom(tableSection).insertBefore(
htmlNode, nextSiblingOfLastAdded);
}
this.updateTabIndexForTableRowNode_(htmlNode);
lastAddedRow = htmlNode;
if (!rowInfo.isExpanded)
continue;
// Append subrows now.
lastAddedRow = this.generateTableRowNodes_(
tableSection, userRow[this.subRowsPropertyName_], rowInfoMap,
indentation + 1, lastAddedRow, rowInfo);
}
return lastAddedRow;
},
getOrCreateRowInfoFor_: function(rowInfoMap, userRow, parentRowInfo) {
var rowInfo = undefined;
if (rowInfoMap.has(userRow)) {
rowInfo = rowInfoMap.get(userRow);
} else {
rowInfo = {
userRow: userRow,
htmlNode: undefined,
parentRowInfo: parentRowInfo
};
rowInfoMap.set(userRow, rowInfo);
}
// Recompute isExpanded in case defaultExpansionStateCallback_ has
// changed.
rowInfo.isExpanded = this.getExpandedForUserRow_(userRow);
return rowInfo;
},
customizeTableRow_: function(userRow, trElement) {
if (!this.customizeTableRowCallback_)
return;
this.customizeTableRowCallback_(userRow, trElement);
},
getHTMLNodeForRowInfo_: function(tableSection, rowInfo,
rowInfoMap, indentation) {
if (rowInfo.htmlNode) {
this.customizeTableRow_(rowInfo.userRow, rowInfo.htmlNode);
return rowInfo.htmlNode;
}
var INDENT_SPACE = indentation * 16;
var INDENT_SPACE_NO_BUTTON = indentation * 16 + BASIC_INDENTATION;
var trElement = this.ownerDocument.createElement('tr');
rowInfo.htmlNode = trElement;
rowInfo.indentation = indentation;
trElement.rowInfo = rowInfo;
this.customizeTableRow_(rowInfo.userRow, trElement);
var isBodyRow = tableSection === this.$.body;
var isExpandableRow = rowInfo.userRow[this.subRowsPropertyName_] &&
rowInfo.userRow[this.subRowsPropertyName_].length;
for (var i = 0; i < this.tableColumns_.length;) {
var td = this.appendNewElement_(trElement, 'td');
td.columnIndex = i;
var column = this.tableColumns_[i];
var value = column.value(rowInfo.userRow);
var colSpan = column.colSpan ? column.colSpan : 1;
td.style.colSpan = colSpan;
switch (column.align) {
case undefined:
case ColumnAlignment.LEFT:
break;
case ColumnAlignment.RIGHT:
td.style.textAlign = 'right';
break;
default:
throw new Error('Invalid alignment of column at index=' + i +
': ' + column.align);
}
if (this.doesColumnIndexSupportSelection(i))
Polymer.dom(td).classList.add('supports-selection');
if (this.columnsWithExpandButtons_.indexOf(i) != -1) {
if (rowInfo.userRow[this.subRowsPropertyName_] &&
rowInfo.userRow[this.subRowsPropertyName_].length > 0) {
td.style.paddingLeft = INDENT_SPACE + 'px';
var expandButton = this.appendNewElement_(td,
'expand-button');
Polymer.dom(expandButton).textContent = RIGHT_ARROW;
if (rowInfo.isExpanded)
Polymer.dom(expandButton).classList.add('button-expanded');
} else {
td.style.paddingLeft = INDENT_SPACE_NO_BUTTON + 'px';
}
}
if (value !== undefined) {
Polymer.dom(td).appendChild(
tr.ui.b.asHTMLOrTextNode(value, this.ownerDocument));
}
// Add a click handler for selection and row expansion (if applicable).
if (isBodyRow || isExpandableRow) {
td.addEventListener('click', function(i, e) {
e.stopPropagation();
if (e.target.tagName === 'EXPAND-BUTTON') {
this.setExpandedForUserRow_(
tableSection, rowInfoMap,
rowInfo.userRow, !rowInfo.isExpanded);
return;
}
// If the row/cell can be selected and it's not selected yet,
// select it.
if (isBodyRow && this.selectionMode_ !== SelectionMode.NONE) {
var shouldSelect = false;
switch (this.selectionMode_) {
case SelectionMode.ROW:
shouldSelect = this.selectedTableRowInfo_ !== rowInfo;
break;
case SelectionMode.CELL:
if (this.doesColumnIndexSupportSelection(i)) {
shouldSelect = this.selectedTableRowInfo_ !== rowInfo ||
this.selectedColumnIndex_ !== i;
}
break;
default:
throw new Error('Invalid selection mode ' +
this.selectionMode_);
}
if (shouldSelect) {
this.didTableRowInfoGetClicked_(rowInfo, i);
return;
}
}
// Otherwise, if the row is expandable, expand/collapse it.
if (isExpandableRow) {
this.setExpandedForUserRow_(tableSection, rowInfoMap,
rowInfo.userRow, !rowInfo.isExpanded);
}
}.bind(this, i));
}
// Add a double-click handler for stepping into a row/cell (if
// applicable).
if (isBodyRow) {
td.addEventListener('dblclick', function(i, e) {
e.stopPropagation();
this.dispatchStepIntoEvent_(rowInfo, i);
}.bind(this, i));
}
i += colSpan;
}
return rowInfo.htmlNode;
},
removeSubNodes_: function(tableSection, rowInfo, rowInfoMap) {
if (rowInfo.userRow[this.subRowsPropertyName_] === undefined)
return;
for (var i = 0;
i < rowInfo.userRow[this.subRowsPropertyName_].length; i++) {
var subRow = rowInfo.userRow[this.subRowsPropertyName_][i];
var subRowInfo = rowInfoMap.get(subRow);
if (!subRowInfo)
continue;
var subNode = subRowInfo.htmlNode;
if (subNode && Polymer.dom(subNode).parentNode === tableSection) {
Polymer.dom(tableSection).removeChild(subNode);
this.removeSubNodes_(tableSection, subRowInfo, rowInfoMap);
}
}
},
scheduleRebuildHeaders_: function() {
this.headerDirty_ = true;
this.scheduleRebuild_();
},
scheduleRebuildBody_: function() {
this.bodyDirty_ = true;
this.scheduleRebuild_();
},
scheduleRebuildFooter_: function() {
this.footerDirty_ = true;
this.scheduleRebuild_();
},
scheduleRebuild_: function() {
if (this.rebuildPending_)
return;
this.rebuildPending_ = true;
setTimeout(function() {
this.rebuildPending_ = false;
this.rebuild();
}.bind(this), 0);
},
rebuildIfNeeded_: function() {
this.rebuild();
},
rebuild: function() {
var wasBodyOrHeaderDirty = this.headerDirty_ || this.bodyDirty_;
if (this.headerDirty_) {
this.generateHeaderColumns_();
this.headerDirty_ = false;
}
if (this.bodyDirty_) {
Polymer.dom(this.$.body).textContent = '';
this.generateTableRowNodes_(
this.$.body,
this.tableRows_, this.tableRowsInfo_, 0,
undefined, undefined);
if (this.tableRows_.length === 0 && this.emptyValue_ !== undefined) {
var trElement = this.ownerDocument.createElement('tr');
Polymer.dom(this.$.body).appendChild(trElement);
Polymer.dom(trElement).classList.add('empty-row');
var td = this.ownerDocument.createElement('td');
Polymer.dom(trElement).appendChild(td);
td.colSpan = this.tableColumns_.length;
var emptyValue = this.emptyValue_;
Polymer.dom(td).appendChild(
tr.ui.b.asHTMLOrTextNode(emptyValue, this.ownerDocument));
}
this.bodyDirty_ = false;
}
if (wasBodyOrHeaderDirty)
this.applySizes_();
if (this.footerDirty_) {
Polymer.dom(this.$.foot).textContent = '';
this.generateTableRowNodes_(
this.$.foot,
this.tableFooterRows_, this.tableFooterRowsInfo_, 0,
undefined, undefined);
if (this.tableFooterRowsInfo_.length) {
Polymer.dom(this.$.body).classList.add('has-footer');
} else {
Polymer.dom(this.$.body).classList.remove('has-footer');
}
this.footerDirty_ = false;
}
},
appendNewElement_: function(parent, tagName) {
var element = parent.ownerDocument.createElement(tagName);
Polymer.dom(parent).appendChild(element);
return element;
},
getExpandedForTableRow: function(userRow) {
this.rebuildIfNeeded_();
var rowInfo = this.tableRowsInfo_.get(userRow);
if (rowInfo === undefined)
throw new Error('Row has not been seen, must expand its parents');
return rowInfo.isExpanded;
},
getExpandedForUserRow_: function(userRow) {
if (userRow[this.subRowsPropertyName_] === undefined)
return false;
if (userRow[this.subRowsPropertyName_].length === 0)
return false;
if (userRow.isExpanded)
return true;
if (userRow.isExpanded === false)
return false;
var rowInfo = this.tableRowsInfo_.get(userRow);
if (rowInfo && rowInfo.isExpanded)
return true;
if (this.defaultExpansionStateCallback_ === undefined)
return false;
var parentUserRow = undefined;
if (rowInfo && rowInfo.parentRowInfo)
parentUserRow = rowInfo.parentRowInfo.userRow;
return this.defaultExpansionStateCallback_(
userRow, parentUserRow);
},
setExpandedForTableRow: function(userRow, expanded) {
this.rebuildIfNeeded_();
var rowInfo = this.tableRowsInfo_.get(userRow);
if (rowInfo === undefined)
throw new Error('Row has not been seen, must expand its parents');
return this.setExpandedForUserRow_(this.$.body, this.tableRowsInfo_,
userRow, expanded);
},
setExpandedForUserRow_: function(tableSection, rowInfoMap,
userRow, expanded) {
this.rebuildIfNeeded_();
var rowInfo = rowInfoMap.get(userRow);
if (rowInfo === undefined)
throw new Error('Row has not been seen, must expand its parents');
rowInfo.isExpanded = !!expanded;
// If no node, then nothing further needs doing.
if (rowInfo.htmlNode === undefined)
return;
// If its detached, then nothing needs doing.
if (rowInfo.htmlNode.parentElement !== tableSection)
return;
// Otherwise, rebuild.
var expandButton =
Polymer.dom(rowInfo.htmlNode).querySelector('expand-button');
if (rowInfo.isExpanded) {
Polymer.dom(expandButton).classList.add('button-expanded');
var lastAddedRow = rowInfo.htmlNode;
if (rowInfo.userRow[this.subRowsPropertyName_]) {
this.generateTableRowNodes_(
tableSection,
rowInfo.userRow[this.subRowsPropertyName_], rowInfoMap,
rowInfo.indentation + 1,
lastAddedRow, rowInfo);
}
} else {
Polymer.dom(expandButton).classList.remove('button-expanded');
this.removeSubNodes_(tableSection, rowInfo, rowInfoMap);
}
this.maybeUpdateSelectedRow_();
},
get selectionMode() {
return this.selectionMode_;
},
set selectionMode(selectionMode) {
if (!tr.b.dictionaryContainsValue(SelectionMode, selectionMode))
throw new Error('Invalid selection mode ' + selectionMode);
this.rebuildIfNeeded_();
this.selectionMode_ = selectionMode;
this.didSelectionStateChange_();
},
get rowHighlightStyle() {
return this.rowHighlightStyle_;
},
set rowHighlightStyle(rowHighlightStyle) {
if (!tr.b.dictionaryContainsValue(HighlightStyle, rowHighlightStyle))
throw new Error('Invalid row highlight style ' + rowHighlightStyle);
this.rebuildIfNeeded_();
this.rowHighlightStyle_ = rowHighlightStyle;
this.didSelectionStateChange_();
},
get resolvedRowHighlightStyle() {
if (this.rowHighlightStyle_ !== HighlightStyle.DEFAULT)
return this.rowHighlightStyle_;
switch (this.selectionMode_) {
case SelectionMode.NONE:
return HighlightStyle.NONE;
case SelectionMode.ROW:
return HighlightStyle.DARK;
case SelectionMode.CELL:
return HighlightStyle.LIGHT;
default:
throw new Error('Invalid selection mode ' + selectionMode);
}
},
get cellHighlightStyle() {
return this.cellHighlightStyle_;
},
set cellHighlightStyle(cellHighlightStyle) {
if (!tr.b.dictionaryContainsValue(HighlightStyle, cellHighlightStyle))
throw new Error('Invalid cell highlight style ' + cellHighlightStyle);
this.rebuildIfNeeded_();
this.cellHighlightStyle_ = cellHighlightStyle;
this.didSelectionStateChange_();
},
get resolvedCellHighlightStyle() {
if (this.cellHighlightStyle_ !== HighlightStyle.DEFAULT)
return this.cellHighlightStyle_;
switch (this.selectionMode_) {
case SelectionMode.NONE:
case SelectionMode.ROW:
return HighlightStyle.NONE;
case SelectionMode.CELL:
return HighlightStyle.DARK;
default:
throw new Error('Invalid selection mode ' + selectionMode);
}
},
setHighlightStyle_: function(highlightAttribute, resolvedHighlightStyle) {
switch (resolvedHighlightStyle) {
case HighlightStyle.NONE:
Polymer.dom(this.$.body).removeAttribute(highlightAttribute);
break;
case HighlightStyle.LIGHT:
Polymer.dom(this.$.body).setAttribute(highlightAttribute, 'light');
break;
case HighlightStyle.DARK:
Polymer.dom(this.$.body).setAttribute(highlightAttribute, 'dark');
break;
default:
throw new Error('Invalid resolved highlight style ' +
resolvedHighlightStyle);
}
},
didSelectionStateChange_: function() {
this.setHighlightStyle_('row-highlight-style',
this.resolvedRowHighlightStyle);
this.setHighlightStyle_('cell-highlight-style',
this.resolvedCellHighlightStyle);
for (var i = 0; i < Polymer.dom(this.$.body).children.length; i++) {
this.updateTabIndexForTableRowNode_(
Polymer.dom(this.$.body).children[i]);
}
this.maybeUpdateSelectedRow_();
},
maybeUpdateSelectedRow_: function() {
if (this.selectedTableRowInfo_ === undefined)
return;
// Selection may be off.
if (this.selectionMode_ === SelectionMode.NONE) {
this.removeSelectedState_();
this.selectedTableRowInfo_ = undefined;
return;
}
// selectedUserRow may not be visible
function isVisible(rowInfo) {
if (!rowInfo.htmlNode)
return false;
return !!rowInfo.htmlNode.parentElement;
}
if (isVisible(this.selectedTableRowInfo_)) {
this.updateSelectedState_();
return;
}
this.removeSelectedState_();
var curRowInfo = this.selectedTableRowInfo_;
while (curRowInfo && !isVisible(curRowInfo))
curRowInfo = curRowInfo.parentRowInfo;
this.selectedTableRowInfo_ = curRowInfo;
if (this.selectedTableRowInfo_)
this.updateSelectedState_();
},
didTableRowInfoGetClicked_: function(rowInfo, columnIndex) {
switch (this.selectionMode_) {
case SelectionMode.NONE:
return;
case SelectionMode.CELL:
if (!this.doesColumnIndexSupportSelection(columnIndex))
return;
if (this.selectedColumnIndex !== columnIndex)
this.selectedColumnIndex = columnIndex;
// Fall through.
case SelectionMode.ROW:
if (this.selectedTableRowInfo_ !== rowInfo)
this.selectedTableRow = rowInfo.userRow;
}
},
dispatchStepIntoEvent_: function(rowInfo, columnIndex) {
var e = new tr.b.Event('step-into');
e.tableRow = rowInfo.userRow;
e.tableColumn = this.tableColumns_[columnIndex];
e.columnIndex = columnIndex;
this.dispatchEvent(e);
},
/**
* If the selectionMode is CELL and a cell is selected,
* return an object containing the row, column, and value of the selected
* cell.
*
* @return {undefined|!Object}
*/
get selectedCell() {
var row = this.selectedTableRow;
var columnIndex = this.selectedColumnIndex;
if (row === undefined || columnIndex === undefined ||
this.tableColumns_.length <= columnIndex)
return undefined;
var column = this.tableColumns_[columnIndex];
return {
row: row,
column: column,
value: column.value(row)
};
},
/**
* If a column is selected, return the object describing the selected
* column.
*
* Columns can be selected independently of rows and cells. So it is
* possible to select column 0 and cell [0,0], or column 1 and cell [0,0],
* for example. See |selectedCell| for how to access the selected cell when
* the selectionMode is CELL.
*
* |selectedTableColumn| is entirely independent of |selectedColumnIndex|.
* When the table selectionMode is CELL, use |selectedTableRow| and
* |selectedColumnIndex| to find the selected cell.
* When one or more columns have |selectable:true|, then use
* |selectedTableColumn| to find the selected column, which may be either
* the same as or different from |selectedColumnIndex|, if a cell is also
* selected.
*
* @return {number|undefined}
*/
get selectedTableColumnIndex() {
var cols = Polymer.dom(this.$.cols).children;
for (var i = 0; i < cols.length; ++i) {
if (cols[i].getAttribute('selected')) {
return i;
}
}
return undefined;
},
/**
* @param {number|undefined} index
*/
set selectedTableColumnIndex(selectedIndex) {
var cols = Polymer.dom(this.$.cols).children;
for (var i = 0; i < cols.length; ++i) {
if (i === selectedIndex)
cols[i].setAttribute('selected', true);
else
cols[i].removeAttribute('selected');
}
},
get selectedTableRow() {
if (!this.selectedTableRowInfo_)
return undefined;
return this.selectedTableRowInfo_.userRow;
},
set selectedTableRow(userRow) {
this.rebuildIfNeeded_();
if (this.selectionMode_ === SelectionMode.NONE)
throw new Error('Selection is off.');
var rowInfo;
if (userRow === undefined) {
rowInfo = undefined;
} else {
rowInfo = this.tableRowsInfo_.get(userRow);
if (!rowInfo)
throw new Error('Row has not been seen, must expand its parents.');
}
var e = this.prepareToChangeSelection_();
this.selectedTableRowInfo_ = rowInfo;
if (this.selectedTableRowInfo_ === undefined) {
this.selectedColumnIndex_ = undefined;
this.removeSelectedState_();
} else {
switch (this.selectionMode_) {
case SelectionMode.ROW:
this.selectedColumnIndex_ = undefined;
break;
case SelectionMode.CELL:
if (this.selectedColumnIndex_ === undefined) {
var i = this.getFirstSelectableColumnIndex_();
if (i == -1)
throw new Error('Cannot find a selectable column.');
this.selectedColumnIndex_ = i;
}
break;
default:
throw new Error('Invalid selection mode ' + this.selectionMode_);
}
this.updateSelectedState_();
}
this.dispatchEvent(e);
},
updateTabIndexForTableRowNode_: function(row) {
if (this.selectionMode_ === SelectionMode.ROW)
row.tabIndex = 0;
else
Polymer.dom(row).removeAttribute('tabIndex');
var enableCellTab = this.selectionMode_ === SelectionMode.CELL;
for (var i = 0; i < this.tableColumns_.length; i++) {
var cell = Polymer.dom(row).children[i];
if (enableCellTab && this.doesColumnIndexSupportSelection(i))
cell.tabIndex = 0;
else
Polymer.dom(cell).removeAttribute('tabIndex');
}
},
prepareToChangeSelection_: function() {
var e = new tr.b.Event('selection-changed');
var previousSelectedRowInfo = this.selectedTableRowInfo_;
if (previousSelectedRowInfo)
e.previousSelectedTableRow = previousSelectedRowInfo.userRow;
else
e.previousSelectedTableRow = undefined;
this.removeSelectedState_();
return e;
},
removeSelectedState_: function() {
this.setSelectedState_(false);
},
updateSelectedState_: function() {
this.setSelectedState_(true);
},
setSelectedState_: function(select) {
if (this.selectedTableRowInfo_ === undefined)
return;
// Row selection.
var rowNode = this.selectedTableRowInfo_.htmlNode;
if (select)
Polymer.dom(rowNode).setAttribute('selected', true);
else
Polymer.dom(rowNode).removeAttribute('selected');
// Cell selection (if applicable).
var cellNode = Polymer.dom(rowNode).children[this.selectedColumnIndex_];
if (!cellNode)
return;
if (select)
Polymer.dom(cellNode).setAttribute('selected', true);
else
Polymer.dom(cellNode).removeAttribute('selected');
},
doesColumnIndexSupportSelection: function(columnIndex) {
var columnInfo = this.tableColumns_[columnIndex];
var scs = columnInfo.supportsCellSelection;
if (scs === false)
return false;
return true;
},
getFirstSelectableColumnIndex_: function() {
for (var i = 0; i < this.tableColumns_.length; i++) {
if (this.doesColumnIndexSupportSelection(i))
return i;
}
return -1;
},
getSelectableNodeGivenTableRowNode_: function(htmlNode) {
switch (this.selectionMode_) {
case SelectionMode.ROW:
return htmlNode;
case SelectionMode.CELL:
return Polymer.dom(htmlNode).children[this.selectedColumnIndex_];
default:
throw new Error('Invalid selection mode ' + this.selectionMode_);
}
},
get selectedColumnIndex() {
if (this.selectionMode_ !== SelectionMode.CELL)
return undefined;
return this.selectedColumnIndex_;
},
set selectedColumnIndex(selectedColumnIndex) {
this.rebuildIfNeeded_();
if (this.selectionMode_ === SelectionMode.NONE)
throw new Error('Selection is off.');
if (selectedColumnIndex < 0 ||
selectedColumnIndex >= this.tableColumns_.length)
throw new Error('Invalid index');
if (!this.doesColumnIndexSupportSelection(selectedColumnIndex))
throw new Error('Selection is not supported on this column');
var e = this.prepareToChangeSelection_();
this.selectedColumnIndex_ = selectedColumnIndex;
if (this.selectedColumnIndex_ === undefined)
this.selectedTableRowInfo_ = undefined;
this.updateSelectedState_();
this.dispatchEvent(e);
},
onKeyDown_: function(e) {
if (this.selectionMode_ === SelectionMode.NONE)
return;
if (this.selectedTableRowInfo_ === undefined)
return;
var code_to_command_names = {
13: 'ENTER',
32: 'SPACE',
37: 'ARROW_LEFT',
38: 'ARROW_UP',
39: 'ARROW_RIGHT',
40: 'ARROW_DOWN'
};
var cmdName = code_to_command_names[e.keyCode];
if (cmdName === undefined)
return;
e.stopPropagation();
e.preventDefault();
this.performKeyCommand_(cmdName);
},
performKeyCommand_: function(cmdName) {
this.rebuildIfNeeded_();
var rowInfo = this.selectedTableRowInfo_;
var htmlNode = rowInfo.htmlNode;
if (cmdName === 'ARROW_UP') {
var prev = htmlNode.previousElementSibling;
if (prev) {
tr.ui.b.scrollIntoViewIfNeeded(prev);
this.selectedTableRow = prev.rowInfo.userRow;
this.focusSelected_();
return;
}
return;
}
if (cmdName === 'ARROW_DOWN') {
var next = htmlNode.nextElementSibling;
if (next) {
tr.ui.b.scrollIntoViewIfNeeded(next);
this.selectedTableRow = next.rowInfo.userRow;
this.focusSelected_();
return;
}
return;
}
if (cmdName === 'ARROW_RIGHT') {
switch (this.selectionMode_) {
case SelectionMode.ROW:
if (rowInfo.userRow[this.subRowsPropertyName_] === undefined)
return;
if (rowInfo.userRow[this.subRowsPropertyName_].length === 0)
return;
if (!rowInfo.isExpanded)
this.setExpandedForTableRow(rowInfo.userRow, true);
this.selectedTableRow =
htmlNode.nextElementSibling.rowInfo.userRow;
this.focusSelected_();
return;
case SelectionMode.CELL:
var newIndex = this.selectedColumnIndex_ + 1;
if (newIndex >= this.tableColumns_.length)
return;
if (!this.doesColumnIndexSupportSelection(newIndex))
return;
this.selectedColumnIndex = newIndex;
this.focusSelected_();
return;
default:
throw new Error('Invalid selection mode ' + this.selectionMode_);
}
}
if (cmdName === 'ARROW_LEFT') {
switch (this.selectionMode_) {
case SelectionMode.ROW:
if (rowInfo.isExpanded) {
this.setExpandedForTableRow(rowInfo.userRow, false);
this.focusSelected_();
return;
}
// Not expanded. Select parent...
var parentRowInfo = rowInfo.parentRowInfo;
if (parentRowInfo) {
this.selectedTableRow = parentRowInfo.userRow;
this.focusSelected_();
return;
}
return;
case SelectionMode.CELL:
var newIndex = this.selectedColumnIndex_ - 1;
if (newIndex < 0)
return;
if (!this.doesColumnIndexSupportSelection(newIndex))
return;
this.selectedColumnIndex = newIndex;
this.focusSelected_();
return;
default:
throw new Error('Invalid selection mode ' + this.selectionMode_);
}
}
if (cmdName === 'SPACE') {
if (rowInfo.userRow[this.subRowsPropertyName_] === undefined)
return;
if (rowInfo.userRow[this.subRowsPropertyName_].length === 0)
return;
this.setExpandedForTableRow(rowInfo.userRow, !rowInfo.isExpanded);
this.focusSelected_();
return;
}
if (cmdName === 'ENTER') {
this.dispatchStepIntoEvent_(rowInfo, this.selectedColumnIndex_);
return;
}
throw new Error('Unrecognized command ' + cmdName);
},
focusSelected_: function() {
if (!this.selectedTableRowInfo_)
return;
var node = this.getSelectableNodeGivenTableRowNode_(
this.selectedTableRowInfo_.htmlNode);
node.focus();
},
dispatchSortingChangedEvent_: function() {
var e = new tr.b.Event('sort-column-changed');
e.sortColumnIndex = this.sortColumnIndex_;
e.sortDescending = this.sortDescending_;
this.dispatchEvent(e);
}
});
})();
</script>
<dom-module id="tr-ui-b-table-header-cell">
<template>
<style>
:host {
-webkit-user-select: none;
display: flex;
}
span {
flex: 0 1 auto;
}
#side {
-webkit-user-select: none;
flex: 0 0 auto;
padding-left: 2px;
padding-right: 2px;
vertical-align: top;
font-size: 15px;
font-family: sans-serif;
line-height: 85%;
margin-left: 5px;
}
#side.disabled {
color: rgb(140, 140, 140);
}
#title:empty, #side:empty {
display: none;
}
</style>
<span id="title"></span>
<span id="side"></span>
</template>
</dom-module>
<script>
'use strict';
var ColumnAlignment = tr.ui.b.TableFormat.ColumnAlignment;
Polymer({
is: 'tr-ui-b-table-header-cell',
created: function() {
this.tapCallback_ = undefined;
this.cellTitle_ = '';
this.align_ = undefined;
this.selectable_ = false;
this.column_ = undefined;
},
ready: function() {
this.addEventListener('click', this.onTap_.bind(this));
},
set column(column) {
this.column_ = column;
this.align = column.align;
this.cellTitle = column.title;
},
get column() {
return this.column_;
},
set cellTitle(value) {
this.cellTitle_ = value;
var titleNode = tr.ui.b.asHTMLOrTextNode(
this.cellTitle_, this.ownerDocument);
this.$.title.innerText = '';
Polymer.dom(this.$.title).appendChild(titleNode);
},
get cellTitle() {
return this.cellTitle_;
},
set align(align) {
switch (align) {
case undefined:
case ColumnAlignment.LEFT:
this.style.justifyContent = '';
break;
case ColumnAlignment.RIGHT:
this.style.justifyContent = 'flex-end';
break;
default:
throw new Error('Invalid alignment of column (title=\'' +
this.cellTitle_ + '\'): ' + align);
}
this.align_ = align;
},
get align() {
return this.align_;
},
clearSideContent: function() {
Polymer.dom(this.$.side).textContent = '';
},
set sideContent(content) {
Polymer.dom(this.$.side).textContent = content;
this.$.side.style.display = content ? 'inline' : 'none';
},
get sideContent() {
return Polymer.dom(this.$.side).textContent;
},
set sideContentDisabled(sideContentDisabled) {
this.$.side.classList.toggle('disabled', sideContentDisabled);
},
get sideContentDisabled() {
return this.$.side.classList.contains('disabled');
},
set tapCallback(callback) {
this.style.cursor = 'pointer';
this.tapCallback_ = callback;
},
get tapCallback() {
return this.tapCallback_;
},
onTap_: function() {
if (this.tapCallback_)
this.tapCallback_();
}
});
</script>