From eaf7107d52454984063d5ed62fce27aed5bca1bc Mon Sep 17 00:00:00 2001 From: mloutraris-ge Date: Mon, 9 Feb 2015 12:06:58 -0500 Subject: [PATCH] Enable Infinite Scroll Up and fix display issue for infinite scroll 'blank block' #2535 Code changes enable infinite scroll up. Changes are as follows: File: misc/tutorial/212_infinite_scroll.ngdoc Update documentation to load the data for up and adjust the load data for down to append to exiting data, not overwrite the hashKeys src/features/infinite-scroll/js/infinite-scroll.js Updated file to add new needLoadMoreDataTop method and supporting code to call method. src/features/infinite-scroll/test/infiniteScroll.spec.js Updated unit test to test for needMoreLoadDataTop src/js/core/constants.js Added new constant for scroll direction src/js/core/directives/ui-grid-viewport.js Added supporting logic to set scroll direction properly src/js/core/directives/ui-grid.js Added new function to adjust scroll position after an infinite scroll data load up or down. src/js/core/factories/Grid.js Added logic to reset scroll direction to none after a debounce as well as increased debounce to 1000 to allow for timeout added to reset scroll position to execute src/js/core/factories/GridRenderContainer.js Added logic too datawatch function to calculate the row to scroll to after a data load. The up scroll is tricky because you ahve to find the row you were on in the new data array. This was done by searching for the hashKey of the row we were on. Supporting code was also added. The "blank block" issue was fixed here by allowing the logic to execute if infinite scroll is enabled. --- misc/tutorial/212_infinite_scroll.ngdoc | 27 +++++++++-- .../infinite-scroll/js/infinite-scroll.js | 35 ++++++++++---- .../test/infiniteScroll.spec.js | 24 ++++++++-- src/js/core/constants.js | 11 ++++- src/js/core/directives/ui-grid-viewport.js | 9 +++- src/js/core/directives/ui-grid.js | 35 +++++++++++++- src/js/core/factories/Grid.js | 21 +++++++-- src/js/core/factories/GridRenderContainer.js | 47 +++++++++++++++---- 8 files changed, 175 insertions(+), 34 deletions(-) diff --git a/misc/tutorial/212_infinite_scroll.ngdoc b/misc/tutorial/212_infinite_scroll.ngdoc index 9b4e27205a..d8f184d9f4 100644 --- a/misc/tutorial/212_infinite_scroll.ngdoc +++ b/misc/tutorial/212_infinite_scroll.ngdoc @@ -32,10 +32,20 @@ Specify percentage when lazy load should trigger: { name:'name' }, { name:'age' } ]; - var page = 1; + var page = 0; + var pageUp = 0; var getData = function(data, page) { var res = []; - for (var i = 0; i < page * 100 && i < data.length; ++i) { + for (var i = (page * 100); i < (page + 1) * 100 && i < data.length; ++i) { + res.push(data[i]); + } + return res; + }; + + var getDataUp = function(data, page) { + var res = []; + for (var i = data.length - (page * 100) - 1; (data.length - i) < ((page + 1) * 100) && (data.length - i) > 0; --i) { + data[i].id = -(data.length - data[i].id) res.push(data[i]); } return res; @@ -51,7 +61,7 @@ Specify percentage when lazy load should trigger: gridApi.infiniteScroll.on.needLoadMoreData($scope,function(){ $http.get('/data/10000_complex.json') .success(function(data) { - $scope.gridOptions.data = getData(data, page); + $scope.gridOptions.data = $scope.gridOptions.data.concat(getData(data, page)); ++page; gridApi.infiniteScroll.dataLoaded(); }) @@ -59,6 +69,17 @@ Specify percentage when lazy load should trigger: gridApi.infiniteScroll.dataLoaded(); }); }); + gridApi.infiniteScroll.on.needLoadMoreDataTop($scope,function(){ + $http.get('/data/10000_complex.json') + .success(function(data) { + $scope.gridOptions.data = getDataUp(data, pageUp).reverse().concat($scope.gridOptions.data); + ++pageUp; + gridApi.infiniteScroll.dataLoaded(); + }) + .error(function() { + gridApi.infiniteScroll.dataLoaded(); + }); + }); }; }]); diff --git a/src/features/infinite-scroll/js/infinite-scroll.js b/src/features/infinite-scroll/js/infinite-scroll.js index 53900e86cf..bd90ae8747 100644 --- a/src/features/infinite-scroll/js/infinite-scroll.js +++ b/src/features/infinite-scroll/js/infinite-scroll.js @@ -17,7 +17,7 @@ * * @description Service for infinite scroll features */ - module.service('uiGridInfiniteScrollService', ['gridUtil', '$compile', '$timeout', function (gridUtil, $compile, $timeout) { + module.service('uiGridInfiniteScrollService', ['gridUtil', '$compile', '$timeout', 'uiGridConstants', function (gridUtil, $compile, $timeout, uiGridConstants) { var service = { @@ -50,6 +50,17 @@ */ needLoadMoreData: function ($scope, fn) { + }, + + /** + * @ngdoc event + * @name needLoadMoreDataTop + * @eventOf ui.grid.infiniteScroll.api:PublicAPI + * @description This event fires when scroll reached top percentage of grid + * and needs to load data + */ + + needLoadMoreDataTop: function ($scope, fn) { } } }, @@ -99,12 +110,16 @@ * @ngdoc function * @name loadData * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService - * @description This function fires 'needLoadMoreData' event + * @description This function fires 'needLoadMoreData' or 'needLoadMoreDataTop' event based on scrollDirection */ loadData: function (grid) { - grid.options.loadTimout = true; - grid.api.infiniteScroll.raise.needLoadMoreData(); + grid.options.loadTimout = true; + if (grid.scrollDirection === uiGridConstants.scrollDirection.UP) { + grid.api.infiniteScroll.raise.needLoadMoreDataTop(); + return; + } + grid.api.infiniteScroll.raise.needLoadMoreData(); }, /** @@ -196,10 +211,14 @@ link: function ($scope, $elm, $attr){ if ($scope.grid.options.enableInfiniteScroll) { $scope.grid.api.core.on.scrollEvent($scope, function (args) { - if (args.y) { - var percentage = 100 - (args.y.percentage * 100); - uiGridInfiniteScrollService.checkScroll($scope.grid, percentage); - } + //Prevent circular scroll references, if source is coming from ui.grid.adjustInfiniteScrollPosition() function + if (args.y && (args.source !== 'ui.grid.adjustInfiniteScrollPosition')) { + var percentage = 100 - (args.y.percentage * 100); + if ($scope.grid.scrollDirection === uiGridConstants.scrollDirection.UP) { + percentage = (args.y.percentage * 100); + } + uiGridInfiniteScrollService.checkScroll($scope.grid, percentage); + } }); } } diff --git a/src/features/infinite-scroll/test/infiniteScroll.spec.js b/src/features/infinite-scroll/test/infiniteScroll.spec.js index e2f0c15be9..dd9af5c09e 100644 --- a/src/features/infinite-scroll/test/infiniteScroll.spec.js +++ b/src/features/infinite-scroll/test/infiniteScroll.spec.js @@ -6,12 +6,14 @@ var uiGridInfiniteScrollService; var grid; var gridClassFactory; + var uiGridConstants; beforeEach(module('ui.grid.infiniteScroll')); - beforeEach(inject(function (_uiGridInfiniteScrollService_, _gridClassFactory_) { + beforeEach(inject(function (_uiGridInfiniteScrollService_, _gridClassFactory_, _uiGridConstants_) { uiGridInfiniteScrollService = _uiGridInfiniteScrollService_; gridClassFactory = _gridClassFactory_; + uiGridConstants = _uiGridConstants_; grid = gridClassFactory.createGrid({}); @@ -24,11 +26,16 @@ gridApi.infiniteScroll.on.needLoadMoreData(function(){ return []; }); - }; + gridApi.infiniteScroll.on.needLoadMoreDataTop(function(){ + return []; + }); + + }; uiGridInfiniteScrollService.initializeGrid(grid); - spyOn(grid.api.infiniteScroll.raise, 'needLoadMoreData'); - + spyOn(grid.api.infiniteScroll.raise, 'needLoadMoreData'); + spyOn(grid.api.infiniteScroll.raise, 'needLoadMoreDataTop'); + grid.options.data = [{col1:'a'},{col1:'b'}]; grid.buildColumns(); @@ -54,7 +61,14 @@ uiGridInfiniteScrollService.loadData(grid); expect(grid.api.infiniteScroll.raise.needLoadMoreData).toHaveBeenCalled(); }); - }); + + it('should call load data top function on grid event raise', function () { + grid.scrollDirection = uiGridConstants.scrollDirection.UP; + uiGridInfiniteScrollService.loadData(grid); + expect(grid.api.infiniteScroll.raise.needLoadMoreDataTop).toHaveBeenCalled(); + }); + + }); }); })(); \ No newline at end of file diff --git a/src/js/core/constants.js b/src/js/core/constants.js index e92b0b1ad2..8905d33898 100644 --- a/src/js/core/constants.js +++ b/src/js/core/constants.js @@ -84,7 +84,16 @@ // TODO(c0bra): Create full list of these somehow. NOTE: do any allow a space before or after them? CURRENCY_SYMBOLS: ['ƒ', '$', '£', '$', '¤', '¥', '៛', '₩', '₱', '฿', '₫'], - + + scrollDirection: { + UP: 'up', + DOWN: 'down', + LEFT: 'left', + RIGHT: 'right', + NONE: 'none' + + }, + dataChange: { ALL: 'all', EDIT: 'edit', diff --git a/src/js/core/directives/ui-grid-viewport.js b/src/js/core/directives/ui-grid-viewport.js index 62656637e0..fe19eec7fd 100644 --- a/src/js/core/directives/ui-grid-viewport.js +++ b/src/js/core/directives/ui-grid-viewport.js @@ -1,8 +1,8 @@ (function(){ 'use strict'; - angular.module('ui.grid').directive('uiGridViewport', ['gridUtil','ScrollEvent', - function(gridUtil, ScrollEvent) { + angular.module('ui.grid').directive('uiGridViewport', ['gridUtil','ScrollEvent','uiGridConstants', + function(gridUtil, ScrollEvent, uiGridConstants) { return { replace: true, scope: {}, @@ -43,6 +43,9 @@ grid.flagScrollingHorizontally(); var xDiff = newScrollLeft - colContainer.prevScrollLeft; + if (xDiff > 0) { grid.scrollDirection = uiGridConstants.scrollDirection.RIGHT; } + if (xDiff < 0) { grid.scrollDirection = uiGridConstants.scrollDirection.LEFT; } + var horizScrollLength = (colContainer.getCanvasWidth() - colContainer.getViewportWidth()); if (horizScrollLength !== 0) { horizScrollPercentage = newScrollLeft / horizScrollLength; @@ -58,6 +61,8 @@ grid.flagScrollingVertically(); var yDiff = newScrollTop - rowContainer.prevScrollTop; + if (yDiff > 0 ) { grid.scrollDirection = uiGridConstants.scrollDirection.DOWN; } + if (yDiff < 0 ) { grid.scrollDirection = uiGridConstants.scrollDirection.UP; } var vertScrollLength = rowContainer.getVerticalScrollLength(); diff --git a/src/js/core/directives/ui-grid.js b/src/js/core/directives/ui-grid.js index a4c38f659d..2a2d56ed91 100644 --- a/src/js/core/directives/ui-grid.js +++ b/src/js/core/directives/ui-grid.js @@ -2,9 +2,9 @@ 'use strict'; angular.module('ui.grid').controller('uiGridController', ['$scope', '$element', '$attrs', 'gridUtil', '$q', 'uiGridConstants', - '$templateCache', 'gridClassFactory', '$timeout', '$parse', '$compile', + '$templateCache', 'gridClassFactory', '$timeout', '$parse', '$compile', 'ScrollEvent', function ($scope, $elm, $attrs, gridUtil, $q, uiGridConstants, - $templateCache, gridClassFactory, $timeout, $parse, $compile) { + $templateCache, gridClassFactory, $timeout, $parse, $compile, ScrollEvent) { // gridUtil.logDebug('ui-grid controller'); var self = this; @@ -59,6 +59,23 @@ } } + function adjustInfiniteScrollPosition (scrollToRow) { + + var scrollEvent = new ScrollEvent(self.grid, null, null, 'ui.grid.adjustInfiniteScrollPosition'); + var totalRows = self.grid.renderContainers.body.visibleRowCache.length; + var percentage = ( scrollToRow + ( scrollToRow / ( totalRows - 1 ) ) ) / totalRows; + + //for infinite scroll, never allow it to be at the zero position so the up button can be active + if ( percentage === 0 ) { + scrollEvent.y = {pixels: 1}; + } + else { + scrollEvent.y = {percentage: percentage}; + } + scrollEvent.fireScrollingEvent(); + + } + function dataWatchFunction(newData) { // gridUtil.logDebug('dataWatch fired'); var promises = []; @@ -97,6 +114,20 @@ $scope.$evalAsync(function() { self.grid.refreshCanvas(true); self.grid.callDataChangeCallbacks(uiGridConstants.dataChange.ROW); + + $timeout(function () { + //Process post load scroll events if using infinite scroll + if ( self.grid.options.enableInfiniteScroll ) { + //If first load, seed the scrollbar down a little to activate the button + if ( self.grid.renderContainers.body.prevRowScrollIndex === 0 ) { + adjustInfiniteScrollPosition(0); + } + //If we are scrolling up, we need to reseed the grid. + if (self.grid.scrollDirection === uiGridConstants.scrollDirection.UP) { + adjustInfiniteScrollPosition(self.grid.renderContainers.body.prevRowScrollIndex + 1 + self.grid.options.excessRows); + } + } + }, 0); }); }); }); diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index a24b328a94..6794983213 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -99,14 +99,25 @@ angular.module('ui.grid') * @description set to true when Grid is scrolling horizontally. Set to false via debounced method */ self.isScrollingHorizontally = false; - + + /** + * @ngdoc property + * @name scrollDirection + * @propertyOf ui.grid.class:Grid + * @description set one of the uiGridConstants.scrollDirection values (UP, DOWN, LEFT, RIGHT, NONE), which tells + * us which direction we are scrolling. Set to NONE via debounced method + */ + self.scrollDirection = uiGridConstants.scrollDirection.NONE; + var debouncedVertical = gridUtil.debounce(function () { self.isScrollingVertically = false; - }, 300); + self.scrollDirection = uiGridConstants.scrollDirection.NONE; + }, 1000); var debouncedHorizontal = gridUtil.debounce(function () { self.isScrollingHorizontally = false; - }, 300); + self.scrollDirection = uiGridConstants.scrollDirection.NONE; + }, 1000); /** @@ -1853,7 +1864,7 @@ angular.module('ui.grid') */ Grid.prototype.refreshCanvas = function(buildStyles) { var self = this; - + if (buildStyles) { self.buildStyles(); } @@ -1969,7 +1980,7 @@ angular.module('ui.grid') // gridUtil.logDebug('redrawing container', i); - container.adjustRows(null, container.prevScrolltopPercentage); + container.adjustRows(null, container.prevScrolltopPercentage, true); container.adjustColumns(null, container.prevScrollleftPercentage); } }; diff --git a/src/js/core/factories/GridRenderContainer.js b/src/js/core/factories/GridRenderContainer.js index 62500b37b7..9eee283324 100644 --- a/src/js/core/factories/GridRenderContainer.js +++ b/src/js/core/factories/GridRenderContainer.js @@ -314,7 +314,7 @@ angular.module('ui.grid') scrollTop = (this.getCanvasHeight() - this.getCanvasWidth()) * scrollPercentage; } - this.adjustRows(scrollTop, scrollPercentage); + this.adjustRows(scrollTop, scrollPercentage, false); this.prevScrollTop = scrollTop; this.prevScrolltopPercentage = scrollPercentage; @@ -339,7 +339,7 @@ angular.module('ui.grid') this.grid.queueRefresh(); }; - GridRenderContainer.prototype.adjustRows = function adjustRows(scrollTop, scrollPercentage) { + GridRenderContainer.prototype.adjustRows = function adjustRows(scrollTop, scrollPercentage, postDataLoaded) { var self = this; var minRows = self.minRowsToRender(); @@ -359,22 +359,53 @@ angular.module('ui.grid') if (rowIndex > maxRowIndex) { rowIndex = maxRowIndex; } - + var newRange = []; if (rowCache.length > self.grid.options.virtualizationThreshold) { if (!(typeof(scrollTop) === 'undefined' || scrollTop === null)) { // Have we hit the threshold going down? - if (self.prevScrollTop < scrollTop && rowIndex < self.prevRowScrollIndex + self.grid.options.scrollThreshold && rowIndex < maxRowIndex) { + if (!self.grid.options.enableInfiniteScroll && self.prevScrollTop < scrollTop && rowIndex < self.prevRowScrollIndex + self.grid.options.scrollThreshold && rowIndex < maxRowIndex) { return; } //Have we hit the threshold going up? - if (self.prevScrollTop > scrollTop && rowIndex > self.prevRowScrollIndex - self.grid.options.scrollThreshold && rowIndex < maxRowIndex) { + if (!self.grid.options.enableInfiniteScroll && self.prevScrollTop > scrollTop && rowIndex > self.prevRowScrollIndex - self.grid.options.scrollThreshold && rowIndex < maxRowIndex) { return; } } - - var rangeStart = Math.max(0, rowIndex - self.grid.options.excessRows); - var rangeEnd = Math.min(rowCache.length, rowIndex + minRows + self.grid.options.excessRows); + var rangeStart = {}; + var rangeEnd = {}; + + //If infinite scroll is enabled, and we loaded more data coming from redrawInPlace, then recalculate the range and set rowIndex to proper place to scroll to + if ( self.grid.options.enableInfiniteScroll && self.grid.scrollDirection !== uiGridConstants.scrollDirection.NONE && postDataLoaded ) { + var findIndex = null; + var i = null; + if ( self.grid.scrollDirection === uiGridConstants.scrollDirection.UP ) { + findIndex = rowIndex > 0 ? self.grid.options.excessRows : 0; + for ( i = 0; i < rowCache.length; i++) { + if (rowCache[i].entity.$$hashKey === self.renderedRows[findIndex].entity.$$hashKey) { + rowIndex = i; + break; + } + } + rangeStart = Math.max(0, rowIndex); + rangeEnd = Math.min(rowCache.length, rangeStart + self.grid.options.excessRows + minRows); + } + else if ( self.grid.scrollDirection === uiGridConstants.scrollDirection.DOWN ) { + findIndex = minRows; + for ( i = 0; i < rowCache.length; i++) { + if (rowCache[i].entity.$$hashKey === self.renderedRows[findIndex].entity.$$hashKey) { + rowIndex = i; + break; + } + } + rangeStart = Math.max(0, rowIndex - self.grid.options.excessRows - minRows); + rangeEnd = Math.min(rowCache.length, rowIndex + minRows + self.grid.options.excessRows); + } + } + else { + rangeStart = Math.max(0, rowIndex - self.grid.options.excessRows); + rangeEnd = Math.min(rowCache.length, rowIndex + minRows + self.grid.options.excessRows); + } newRange = [rangeStart, rangeEnd]; }