Compare commits

...

1 Commits

Author SHA1 Message Date
a7d322d01c [Table] Add datatable bundle
Add the datatable bundle which takes care of the basics of displaying
tabular data.
2016-02-11 11:45:57 -08:00
3 changed files with 451 additions and 0 deletions

View File

@ -0,0 +1,45 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define([
"./src/directives/MCTDataTable",
"legacyRegistry"
], function (
MCTDataTable,
legacyRegistry
) {
"use strict";
legacyRegistry.register("platform/datatable", {
"extensions": {
"directives": [
{
"key": "mctDataTable",
"implementation": MCTDataTable,
"depends": ["$timeout"]
}
]
}
});
});

View File

@ -0,0 +1,65 @@
<div class="l-view-section scrolling"
ng-style="overrideRowPositioning ?
{'overflow': 'auto'} :
{'overflow': 'scroll'}
"
>
<table class="filterable" ng-style="overrideRowPositioning && {
height: totalHeight + 'px',
'table-layout': overrideRowPositioning ? 'fixed' : 'auto',
'box-sizing': 'border-box'
}">
<thead>
<tr>
<th ng-repeat="header in displayHeaders"
ng-style="overrideRowPositioning && {
width: columnWidths[$index] + 'px',
'max-width': columnWidths[$index] + 'px',
overflow: 'none',
'box-sizing': 'border-box'
}"
ng-class="[
enableSort ? 'sortable' : '',
sortColumn === header ? 'sort' : '',
sortDirection || ''
].join(' ')"
ng-click="toggleSort(header)">
{{ header }}
</th>
</tr>
<tr ng-if="enableFilter" class="s-filters">
<th ng-repeat="header in displayHeaders"
ng-style="overrideRowPositioning && {
width: columnWidths[$index] + 'px',
'max-width': columnWidths[$index] + 'px',
overflow: 'none',
'box-sizing': 'border-box'
}">
<input type="text"
ng-model="filters[header]"/>
</th>
</tr>
</thead>
<tbody ng-style="overrideRowPositioning || {
'opacity': 0.0
}">
<tr ng-repeat="visibleRow in visibleRows track by visibleRow.rowIndex"
ng-style="overrideRowPositioning && {
position: 'absolute',
top: visibleRow.offsetY + 'px',
}">
<td ng-repeat="header in displayHeaders"
ng-style="overrideRowPositioning && {
width: columnWidths[$index] + 'px',
'max-width': columnWidths[$index] + 'px',
overflow: 'none',
'box-sizing': 'border-box'
}">
{{ visibleRow.contents[header] }}
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -0,0 +1,341 @@
/*global define*/
define(
[],
function () {
"use strict";
function MCTDataTable($timeout) {
/**
* Set default values for optional parameters on a given scope
*/
function setDefaults($scope) {
if (typeof $scope.enableFilter === 'undefined') {
$scope.enableFilter = true;
$scope.filters = {};
}
if (typeof $scope.enableSort === 'undefined') {
$scope.enableSort = true;
$scope.sortColumn = undefined;
$scope.sortDirection = undefined;
}
}
function link($scope, element) {
setDefaults($scope);
var maxDisplayRows = 50;
$scope.visibleRows = [];
$scope.overrideRowPositioning = false;
/**
* Returns true if row matches all filters.
*/
function matchRow(filters, row) {
return Object.keys(filters).every(function(key) {
if (!row[key]) {
return false;
}
var testVal = String(row[key]).toLowerCase();
return testVal.indexOf(filters[key]) !== -1;
});
}
/**
* Filter rows.
*/
function filterRows(rowsToFilter) {
if (!Object.keys($scope.filters).length) {
return rowsToFilter;
}
var filters = {};
Object.keys($scope.filters).forEach(function(key) {
if (!$scope.filters[key]) {
return;
}
filters[key] = $scope.filters[key].toLowerCase();
});
return rowsToFilter.filter(matchRow.bind(null, filters));
}
/**
* Compare two variables, returning a number that represents
* which is larger. Similar to the default array sort
* comparator, but does not coerce all values to string before
* conversion. Strings are lowercased before comparison.
*/
function genericComparator(a, b) {
if (typeof a === "string" && typeof b === "string") {
a = a.toLowerCase();
b = b.toLowerCase();
}
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
}
/**
* Returns a new array which is a result of applying the sort
* criteria defined in $scope.
*
* Does not modify the array that was passed in.
*/
function sortRows(rowsToSort) {
if (!$scope.sortColumn || !$scope.sortDirection) {
return rowsToSort;
}
var sortKey = $scope.sortColumn,
sortDirectionMultiplier;
if ($scope.sortDirection === 'asc') {
sortDirectionMultiplier = 1;
} else if ($scope.sortDirection === 'desc') {
sortDirectionMultiplier = -1;
}
return rowsToSort.slice(0).sort(function(a, b) {
return genericComparator(a[sortKey], b[sortKey]) *
sortDirectionMultiplier;
});
}
/**
* Returns an object which contains the largest values
* for each key in the given set of rows. This is used to
* pre-calculate optimal column sizes without having to render
* every row.
*/
function findLargestRow(rows) {
var largestRow = rows.reduce(function (largestRow, row) {
Object.keys(row).forEach(function (key) {
var currentColumnLength =
(row[key] && row[key].length) ?
row[key].length :
row[key],
largestColumnLength =
(largestRow[key] && largestRow[key].length) ?
largestRow[key].length :
largestRow[key];
if (currentColumnLength > largestColumnLength) {
largestRow[key] = row[key];
}
});
return largestRow;
}, JSON.parse(JSON.stringify(rows[0])));
// Pad with characters to accomodate variable-width fonts,
// and remove characters that would allow word-wrapping.
Object.keys(largestRow).forEach(function(key) {
var padCharacters,
i;
largestRow[key] = String(largestRow[key]);
padCharacters = largestRow[key].length / 10;
for (i = 0; i < padCharacters; i++) {
largestRow[key] = largestRow[key] + 'W';
}
largestRow[key] = largestRow[key]
.replace(/[ \-_]/g, 'W');
});
return largestRow;
}
/**
* Read styles from the DOM and use them to calculate offsets
* for individual rows.
*/
function setElementSizes() {
var thead = element.find('thead'),
tbody = element.find('tbody'),
firstRow = tbody.find('tr'),
column = firstRow.find('td'),
headerHeight = thead.prop('offsetHeight'),
rowHeight = firstRow.prop('offsetHeight'),
overallHeight = headerHeight + (rowHeight * ($scope.displayRows ? $scope.displayRows.length - 1 : 0));
$scope.columnWidths = [];
while (column.length) {
$scope.columnWidths.push(column.prop('offsetWidth'));
column = column.next();
}
$scope.headerHeight = headerHeight;
$scope.rowHeight = rowHeight;
$scope.totalHeight = overallHeight;
$scope.visibleRows = $scope.displayRows.slice(0, maxDisplayRows).map(function(row, i) {
return {
rowIndex: i,
offsetY: (i * $scope.rowHeight) + $scope.headerHeight,
contents: row
};
});
$scope.overrideRowPositioning = true;
}
/**
* Update rows with new data. If filtering is enabled, rows
* will be sorted before display.
*/
function updateRows(newRows) {
var largestRow;
$scope.visibleRows = [];
$scope.displayRows = [];
$scope.overrideRowPositioning = false;
if (!$scope.displayHeaders) {
return;
}
if ($scope.enableFilter) {
$scope.displayRows = newRows = filterRows(newRows);
}
if ($scope.enableSort) {
$scope.displayRows = newRows = sortRows(newRows);
}
largestRow = findLargestRow(newRows);
$scope.visibleRows = [
{
rowIndex: 0,
offsetY: undefined,
contents: largestRow
}
];
$timeout(setElementSizes, 0);
}
/**
* Update table headers with new headers. If filtering is
* enabled, reset filters. If sorting is enabled, reset
* sorting.
*/
function updateHeaders(newHeaders) {
$scope.displayHeaders = newHeaders;
if ($scope.enableFilter) {
$scope.filters = {};
}
if ($scope.enableSort) {
$scope.sortColumn = undefined;
$scope.sortDirection = undefined;
}
}
/**
* On scroll, calculate which rows indexes are visible and
* ensure that an equal number of rows are preloaded for
* scrolling in either direction.
*/
function onScroll(event) {
var topScroll = event.target.scrollTop,
bottomScroll = topScroll + event.target.offsetHeight,
firstVisible,
lastVisible,
totalVisible,
numberOffscreen,
start,
end;
if ($scope.displayRows.length < maxDisplayRows) {
return;
}
if (topScroll < $scope.headerHeight) {
firstVisible = 0;
} else {
firstVisible = Math.floor(
(topScroll - $scope.headerHeight) / $scope.rowHeight
);
}
lastVisible = Math.ceil(
(bottomScroll - $scope.headerHeight) / $scope.rowHeight
);
totalVisible = lastVisible - firstVisible;
numberOffscreen = maxDisplayRows - totalVisible;
start = firstVisible - Math.floor(numberOffscreen / 2);
end = lastVisible + Math.ceil(numberOffscreen / 2);
if (start < 0) {
start = 0;
end = $scope.visibleRows.length - 1;
} else if (end >= $scope.displayRows.length) {
end = $scope.displayRows.length - 1;
start = end - maxDisplayRows + 1;
}
if ($scope.visibleRows[0].rowIndex === start &&
$scope.visibleRows[$scope.visibleRows.length-1]
.rowIndex === end) {
return; // don't update if no changes are required.
}
$scope.visibleRows = $scope.displayRows.slice(start, end)
.map(function(row, i) {
return {
rowIndex: start + i,
offsetY: ((start + i) * $scope.rowHeight) +
$scope.headerHeight,
contents: row
};
});
$scope.$digest();
}
element.find('div').on('scroll', onScroll);
$scope.toggleSort = function (key) {
if (!$scope.enableSort) {
return;
}
if ($scope.sortColumn !== key) {
$scope.sortColumn = key;
$scope.sortDirection = 'asc';
} else if ($scope.sortDirection === 'asc') {
$scope.sortDirection = 'desc';
} else if ($scope.sortDirection === 'desc') {
$scope.sortColumn = undefined;
$scope.sortDirection = undefined;
}
updateRows($scope.rows);
};
$scope.$watchCollection('filters', function () {
updateRows($scope.rows);
});
$scope.$watch('headers', updateHeaders);
$scope.$watch('rows', updateRows);
}
return {
restrict: "E",
templateUrl: "platform/datatable/res/templates/mct-data-table.html",
link: link,
scope: {
headers: "=",
rows: "=",
enableFilter: "=?",
enableSort: "=?"
}
};
}
return MCTDataTable;
}
);