mirror of
https://github.com/nasa/openmct.git
synced 2025-06-25 10:44:21 +00:00
Compare commits
1 Commits
plotly-imp
...
tabular-vi
Author | SHA1 | Date | |
---|---|---|---|
a7d322d01c |
45
platform/datatable/bundle.js
Normal file
45
platform/datatable/bundle.js
Normal 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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
});
|
65
platform/datatable/res/templates/mct-data-table.html
Normal file
65
platform/datatable/res/templates/mct-data-table.html
Normal 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>
|
341
platform/datatable/src/directives/MCTDataTable.js
Normal file
341
platform/datatable/src/directives/MCTDataTable.js
Normal 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;
|
||||
}
|
||||
);
|
Reference in New Issue
Block a user