Skip to content

Commit

Permalink
Merge pull request #64 from wvary/improve-scrolling-performance.SPOI-…
Browse files Browse the repository at this point in the history
…10007

SPOI-10007 #resolve : Improved performance of table when scrolling
  • Loading branch information
Sasha authored May 5, 2017
2 parents 70be9f2 + 8c470c6 commit 25a4fe3
Show file tree
Hide file tree
Showing 16 changed files with 331 additions and 180 deletions.
21 changes: 18 additions & 3 deletions app/scripts/controllers/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,16 @@ angular.module('datatorrent.mlhrTable.ghPage')
// Generates `num` random rows
function genRows(num){
var retVal = [];
var id = 0;
if ($scope.my_table_data.length > 0) {
id = $scope.my_table_data.map(function(row) {
return row.id;
}).sort(function(a, b) {
return b - a;
})[0] + 1;
}
for (var i=0; i < num; i++) {
retVal.push(genRow(i));
retVal.push(genRow(id++));
}
return retVal;
}
Expand Down Expand Up @@ -183,15 +191,22 @@ angular.module('datatorrent.mlhrTable.ghPage')
$scope.my_table_data = genRows(1000);

$scope.autoRefresh = false;
$scope.autoAppend = false;

// kick off interval that updates the dataset
setInterval(function() {
if ($scope.autoRefresh) {
$scope.my_table_data = genRows(1000);
$scope.my_table_data.length = 0;
$scope.my_table_data.push.apply($scope.my_table_data, genRows(1000));
dataDfd.resolve();
$scope.$apply();
}
else if ($scope.autoAppend) {
$scope.my_table_data.push.apply($scope.my_table_data, genRows(1000));
dataDfd.resolve();
$scope.$apply();
}
}, 1000);
}, 2000);

$scope.removeHalf = function() {
$scope.my_table_data.length = Math.ceil($scope.my_table_data.length / 2);
Expand Down
4 changes: 3 additions & 1 deletion app/views/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ <h5>Kitchen Sink Example:</h5>
<div>
<button class="btn btn-default btn-sm" ng-click="removeHalf()">remove half</button>
&nbsp;&nbsp;&nbsp;
<input id="autoRefresh" type="checkbox" ng-model="autoRefresh" /> <label for="autoRefresh">Auto refresh data</label>
<input id="autoRefresh" type="checkbox" ng-model="autoRefresh" ng-click="autoAppend = false;" /> <label for="autoRefresh">Auto refresh data</label>
&nbsp;&nbsp;&nbsp;
<input id="autoAppend" type="checkbox" ng-model="autoAppend" ng-click="autoRefresh = false;" /> <label for="autoAppend">Auto append data</label>
</div>

Selected rows:
Expand Down
2 changes: 1 addition & 1 deletion bower.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "malhar-angular-table",
"version": "1.6.7",
"version": "2.0.0",
"main": "./dist/mlhr-table.js",
"dependencies": {
"angular": "~1.3",
Expand Down
21 changes: 21 additions & 0 deletions dist/mlhr-table.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,27 @@
/* the visible table header */

.mlhr-header-table {
background-color: white;
/* need background white since header is on top of visible rows */
border-bottom: none;
position: sticky;
/* this is used to float header at top of div (equivalent to relative + fixed) */
position: -webkit-sticky;
/* for safari */
top: 0px;
/* float at top of div */
z-index: 99;
/* z-index of 99 to show on top of visible rows */
}

.mlhr-header-table thead > tr > th {
border-width: 1px;
}

.mlhr-header-table thead > tr:last-child > td {
border-bottom: 1px solid #CCC;
}

/* the invisible table header; used for correct column widths */

.mlhr-rows-table thead {
Expand All @@ -37,6 +51,13 @@
overflow: auto;
}

.mlhr-rows-table {
background-color: white;
/* need backgroudn white because this is on top of dummy rows */
position: relative;
/* position relative to div so it can be moved up/down according to div scroll top */
}

.mlhr-rows-table > tbody + tbody {
border-top: none;
}
Expand Down
151 changes: 98 additions & 53 deletions dist/mlhr-table.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/mlhr-table.min.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/mlhr-table.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "malhar-angular-table",
"version": "1.6.7",
"version": "2.0.0",
"license": "Apache License, v2.0",
"dependencies": {},
"devDependencies": {
Expand Down
66 changes: 44 additions & 22 deletions src/directives/mlhrTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ angular.module('datatorrent.mlhrTable.directives.mlhrTable', [
defaults(scope.options, {
bgSizeMultiplier: 1,
rowPadding: 10,
headerHeight: 77,
bodyHeight: 300,
fixedHeight: false,
defaultRowHeight: 40,
Expand Down Expand Up @@ -172,14 +173,12 @@ angular.module('datatorrent.mlhrTable.directives.mlhrTable', [
scope.$watch('options.pagingScheme', scope.saveToStorage);
// - row limit
scope.$watch('options.bodyHeight', function() {
var headerHeight = scope.tableHeader ? scope.tableHeader.height() || scope.options.headerHeight : scope.options.headerHeight;
scope.calculateRowLimit();
scope.tbodyNgStyle = {};
scope.tbodyNgStyle[ scope.options.fixedHeight ? 'height' : 'max-height' ] = scope.options.bodyHeight + 'px';
scope.tbodyNgStyle[ scope.options.fixedHeight ? 'height' : 'max-height' ] = (scope.options.bodyHeight + headerHeight) + 'px';
scope.saveToStorage();
});
scope.$watch('filterState.filterCount', function() {
scope.onScroll();
});
scope.$watch('rowHeight', function(size) {
element.find('tr.mlhr-table-dummy-row').css('background-size','auto ' + size * scope.options.bgSizeMultiplier + 'px');
});
Expand All @@ -188,7 +187,34 @@ angular.module('datatorrent.mlhrTable.directives.mlhrTable', [
}

var scrollDeferred;
var debouncedScrollHandler = debounce(function() {
var scrollTopSaved = -1;

// determine requestAnimationFrame compabitility
var raf = window.requestAnimationFrame
|| window.mozRequestAnimationFrame
|| window.webkitRequestAnimationFrame
|| window.msRequestAnimationFrame
|| function(f) { return setTimeout(f, scope.options.scrollDebounce) };

var loop = function(timeStamp) {
if (scrollTopSaved !== scope.scrollDiv.scrollTop()) {
scope.tableHeader = scope.tableHeader || element.find('.mlhr-table.mlhr-header-table');
scope.tableDummy = scope.tableDummy || element.find('.mlhr-table.mlhr-dummy-table.table');
scope.tableRows = scope.tableRows || element.find('.mlhr-table.mlhr-rows-table.table');

scrollTopSaved = scope.scrollDiv.scrollTop();
if (!scrollDeferred) {
scrollDeferred = $q.defer();
scope.options.scrollingPromise = scrollDeferred.promise;
}
// perform scrolling code
scope.scrollHandler();
}
// add loop to next repaint cycle
raf(loop);
};

scope.scrollHandler = function() {

scope.calculateRowLimit();

Expand All @@ -200,37 +226,33 @@ angular.module('datatorrent.mlhrTable.directives.mlhrTable', [
return false;
}

scope.rowOffset = Math.max(0, Math.floor(scrollTop / rowHeight) - scope.options.rowPadding);

scrollDeferred.resolve();
// make sure we adjust rowOffset so that last row renders at bottom of div
scope.rowOffset = Math.max(0, Math.min(scope.filterState.filterCount - scope.rowLimit, Math.floor(scrollTop / rowHeight) - scope.options.rowPadding));

scrollDeferred = null;
// move the table rows to a position according to the div scroll top
scope.tableRows.css('top', '-' + (scope.tableDummy.height() - rowHeight * scope.rowOffset) + 'px');

if (scrollDeferred) {
scrollDeferred.resolve();
scrollDeferred = null;
}
scope.options.scrollingPromise = null;

scope.$digest();

}, scope.options.scrollDebounce);

scope.onScroll = function() {
if (!scrollDeferred) {
scrollDeferred = $q.defer();
scope.options.scrollingPromise = scrollDeferred.promise;
if (!scope.$root.$$phase) {
scope.$digest();
}
debouncedScrollHandler();
scope.userScrollSaved = scope.userScroll;
};

scope.scrollDiv = element.find('.mlhr-rows-table-wrapper');
scope.scrollDiv.on('scroll', scope.onScroll);

raf(loop);

// Wait for a render
$timeout(function() {
// Calculates rowHeight and rowLimit
scope.calculateRowLimit();

}, 0);


scope.api = {
isSelectedAll: scope.isSelectedAll,
selectAll: scope.selectAll,
Expand Down
23 changes: 17 additions & 6 deletions src/directives/mlhrTableDummyRows.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
* @restrict A
* @description inserts dummy <tr>s for non-rendered rows
* @element tbody
* @example <tbody mlhr-table-dummy-rows="[number]" columns="[column array]"></tbody>
* @example <tbody mlhr-table-dummy-rows mlhr-table-dummy-rows-filtered-count="filteredState.filterCount" mlhr-table-dummy-rows-visible-count="visible_rows.length" columns="[column array]"></tbody>
**/
angular.module('datatorrent.mlhrTable.directives.mlhrTableDummyRows', [])
.directive('mlhrTableDummyRows', function() {
Expand All @@ -30,12 +30,23 @@ angular.module('datatorrent.mlhrTable.directives.mlhrTableDummyRows', [])
template: '<tr class="mlhr-table-dummy-row" ng-style="{ height: dummyRowHeight + \'px\'}"><td ng-show="dummyRowHeight" ng-attr-colspan="{{columns.length}}"></td></tr>',
scope: true,
link: function(scope, element, attrs) {

scope.$watch(attrs.mlhrTableDummyRows, function(count) {
scope.dummyRowHeight = count * scope.rowHeight;
function updateHeight() {
if (scope.$parent.tableRows) {
scope.dummyRowHeight = (scope.$parent.filterState.filterCount - scope.$parent.visible_rows.length) * scope.rowHeight;
var rowHeight = scope.$parent.tableRows.height() / scope.$parent.visible_rows.length;
scope.$parent.tableRows.css('top', '-' + (scope.dummyRowHeight - rowHeight * scope.$parent.rowOffset) + 'px');
}
}
scope.$watch(attrs.mlhrTableDummyRowsFilteredCount, function(newVal, oldVal) {
if (newVal !== oldVal) {
updateHeight();
}
});
scope.$watch(attrs.mlhrTableDummyRowsVisibleCount, function(newVal, oldVal) {
if (newVal !== oldVal) {
updateHeight();
}
});

}
};

});
75 changes: 45 additions & 30 deletions src/directives/mlhrTableRows.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,33 @@ angular.module('datatorrent.mlhrTable.directives.mlhrTableRows',[
var limitTo = $filter('limitTo');

function calculateVisibleRows(scope) {
// scope.rows
// store visible rows in this variable
var visible_rows;

// | tableRowFilter:columns:searchTerms:filterState
visible_rows = tableRowFilter(scope.rows, scope.columns, scope.searchTerms, scope.filterState, scope.options);

// | tableRowSorter:columns:sortOrder:sortDirection
visible_rows = tableRowSorter(visible_rows, scope.columns, scope.sortOrder, scope.sortDirection, scope.options);

// | limitTo:rowOffset - filterState.filterCount
visible_rows = limitTo(visible_rows, Math.floor(scope.rowOffset) - scope.filterState.filterCount);
// build cache key using search terms and sorting options
var cacheKey = JSON.stringify({
searchTerms: scope.searchTerms,
sortOrder: scope.sortOrder,
sortDirection: scope.sortDirection
});

// | limitTo:rowLimit
// initialize cache if necessary
scope.filterState.cache = scope.filterState.cache || {};

// filter and sort if not in cache
if (!scope.filterState.cache[cacheKey]) {
scope.filterState.cache[cacheKey]= scope.filterState.cache[cacheKey] || tableRowFilter(scope.rows, scope.columns, scope.searchTerms, scope.filterState, scope.options);
scope.filterState.cache[cacheKey] = tableRowSorter(scope.filterState.cache[cacheKey], scope.columns, scope.sortOrder, scope.sortDirection, scope.options);
}

// update filter count
scope.filterState.filterCount = scope.filterState.cache[cacheKey].length;

// get visible rows from filter cache
visible_rows = limitTo(scope.filterState.cache[cacheKey], Math.floor(scope.rowOffset) - scope.filterState.filterCount);


// set upper limit if necessary
visible_rows = limitTo(visible_rows, scope.rowLimit + Math.ceil(scope.rowOffset % 1));

return visible_rows;
Expand Down Expand Up @@ -79,34 +93,35 @@ angular.module('datatorrent.mlhrTable.directives.mlhrTableRows',[
}
};

var highlightRowHandler = function() {
if (scope.rows) {
if (scope.options.highlightRow) {
// there is a highlightRow function, execute it
for (var i = 0; i < scope.rows.length; i++) {
scope.rows[i].highlight = scope.options.highlightRow(scope.rows[i]);
}
} else {
// there isn't a highlightRow function, set property to false
for (var i = 0; i < scope.rows.length; i++) {
scope.rows[i].highlight = false;
}
}
}
scope.highlightRowHandler = function(row) {
return (scope.options.highlightRow ? scope.options.highlightRow(row) : false);
};

scope.$watch('searchTerms', updateHandler, true);
scope.$watch('searchTerms', function() {
if (scope.scrollDiv.scrollTop() !== 0) {
// on filter change, scroll to top, let the scroll event update the view
scope.scrollDiv.scrollTop(0);
} else {
// no scroll change, run updateHandler
updateHandler();
}
}, true);
scope.$watch('[filterState.filterCount,rowOffset,rowLimit]', updateHandler);
scope.$watch('sortOrder', updateHandler, true);
scope.$watch('sortDirection', updateHandler, true);
scope.$watch('rows', function(){
highlightRowHandler();
scope.$watch('rows', function(newVal, oldVal){
// clear cache when data changes
scope.filterState.cache = {};

updateSelection();
updateHandler();

if (angular.isArray(newVal) && angular.isArray(oldVal) && newVal.length < oldVal.length) {
// because row count is reducing, we should perform scrollHandler to see if we need to
// change scrolling or visible rows
scope.scrollHandler();
}
}, true);
scope.$watch('options.highlightRow', function(newVal, oldVal) {
highlightRowHandler();
});
}

return {
Expand Down
2 changes: 1 addition & 1 deletion src/directives/mlhrTableSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ angular.module('datatorrent.mlhrTable.directives.mlhrTableSelector', [])
scope: false,
link: function postLink(scope, element) {
var selected = scope.selected;
var row = scope.row;
var column = scope.column;
element.on('click', function() {
var row = scope.row;
scope.options.__selectionColumn = column;

// Retrieve position in selected list
Expand Down
Loading

0 comments on commit 25a4fe3

Please sign in to comment.