From eac2c230c38c0c050fb9b60ae09dcc4a14253331 Mon Sep 17 00:00:00 2001 From: "Kondratev, Gennadii" Date: Thu, 6 Oct 2016 18:39:36 +0300 Subject: [PATCH] feat(Grid Scrolling): Adds the ability to overwrite default grid scroll event. Creating a wrapper service that takes over the default scrolling logic in order to ensure that grid scrolling works well in devices. Also, adds the ability to the grid to allow users to provide their own custom scrolling. --- misc/tutorial/401_AllFeatures.ngdoc | 1 + misc/tutorial/406_custom_pagination.ngdoc | 120 +++++ misc/tutorial/407_custom_scrolling.ngdoc | 59 +++ src/features/cellnav/js/cellnav.js | 10 +- .../custom-scrolling/js/custom-scrolling.js | 415 ++++++++++++++++++ ...ui-grid-custom-scrolling.directive.spec.js | 30 ++ .../test/ui-grid-scroller.factory.spec.js | 213 +++++++++ src/features/edit/js/gridEdit.js | 4 +- .../infinite-scroll/js/infinite-scroll.js | 9 +- src/features/pagination/js/pagination.js | 66 ++- .../pagination/templates/pagination.html | 6 +- .../pagination/test/pagination.spec.js | 109 +++++ src/features/selection/js/selection.js | 5 + src/js/core/constants.js | 1 - src/js/core/directives/ui-grid-viewport.js | 24 +- src/js/core/directives/ui-grid.js | 2 +- src/js/core/factories/Grid.js | 4 +- src/js/core/services/rowSorter.js | 2 +- src/js/core/services/ui-grid-util.js | 8 +- src/less/body.less | 1 + 20 files changed, 1036 insertions(+), 53 deletions(-) create mode 100644 misc/tutorial/406_custom_pagination.ngdoc create mode 100644 misc/tutorial/407_custom_scrolling.ngdoc create mode 100644 src/features/custom-scrolling/js/custom-scrolling.js create mode 100644 src/features/custom-scrolling/test/ui-grid-custom-scrolling.directive.spec.js create mode 100644 src/features/custom-scrolling/test/ui-grid-scroller.factory.spec.js diff --git a/misc/tutorial/401_AllFeatures.ngdoc b/misc/tutorial/401_AllFeatures.ngdoc index 6393ef8e2f..48670c0af8 100644 --- a/misc/tutorial/401_AllFeatures.ngdoc +++ b/misc/tutorial/401_AllFeatures.ngdoc @@ -18,6 +18,7 @@ All features are enabled to get an idea of performance $scope.gridOptions = {}; $scope.gridOptions.data = 'myData'; + $scope.gridOptions.enableCellEditOnFocus = true; $scope.gridOptions.enableColumnResizing = true; $scope.gridOptions.enableFiltering = true; $scope.gridOptions.enableGridMenu = true; diff --git a/misc/tutorial/406_custom_pagination.ngdoc b/misc/tutorial/406_custom_pagination.ngdoc new file mode 100644 index 0000000000..b2623c00d1 --- /dev/null +++ b/misc/tutorial/406_custom_pagination.ngdoc @@ -0,0 +1,120 @@ +@ngdoc overview +@name Tutorial: 406 Custom Pagination +@description + +When pagination is enabled, the data is displayed in pages that can be browsed using the built in +pagination selector. However, you don't always have pages with the same number of rows. + +For custom pagination, set the `pageSizes` option and the `useCustomPagination`. +~~ and implement the `gridApi.pagination.on.paginationChanged` callback function. The callback +may contain code to update any pagination state variables your application may utilize, e.g. variables containing +the `pageNumber`, `pageSize`, and `pageSizeList`. The REST call used to fetch the data from the server should be +called from within this callback. The URL of the call should contain query parameters that will allow the server-side +code to have sufficient information to be able to retrieve the specific subset of data that the client requires from +the entire set.~~ + +It should also update the `$scope.gridOptions.totalItems` variable with the total count of rows that exist (but +were not all fetched in the REST call mentioned above since they exist in other pages of data). + +This will allow ui-grid to calculate the correct number of pages on the client-side. + +@example +This shows custom pagination. + + + var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.pagination']); + + app.controller('MainCtrl', [ + '$scope', '$http', 'uiGridConstants', function($scope, $http, uiGridConstants) { + + $scope.gridOptions1 = { + paginationPageSizes: null, + useCustomPagination: true, + columnDefs: [ + { name: 'name', enableSorting: false }, + { name: 'gender', enableSorting: false }, + { name: 'company', enableSorting: false } + ] + }; + + $scope.gridOptions2 = { + paginationPageSizes: null, + useCustomPagination: true, + useExternalPagination : true, + columnDefs: [ + { name: 'name', enableSorting: false }, + { name: 'gender', enableSorting: false }, + { name: 'company', enableSorting: false } + ], + onRegisterApi: function(gridApi) { + gridApi.pagination.on.paginationChanged($scope, function (pageNumber, pageSize) { + $scope.gridOptions2.data = getPage($scope.grid2data, pageNumber); + }); + } + }; + + $http.get('/data/100_ASC.json') + .success(function (data) { + $scope.gridOptions1.data = data; + $scope.gridOptions1.paginationPageSizes = calculatePageSizes(data); + }); + + $http.get('/data/100.json') + .success(function (data) { + $scope.grid2data = data; + $scope.gridOptions2.totalItems = 0;//data.length; + $scope.gridOptions2.paginationPageSizes = calculatePageSizes(data); + $scope.gridOptions2.data = getPage($scope.grid2data, 1); + }); + + + function calculatePageSizes(data) { + var initials = []; + return data.reduce(function(pageSizes, row) { + var initial = row.name.charAt(0); + var index = initials.indexOf(initial); + if(index < 0) + { + index = initials.length; + initials.push(initial); + } + pageSizes[index] = (pageSizes[index] || 0) + 1; + return pageSizes; + }, []); + } + + function getPage(data, pageNumber) + { + var initials = []; + return data.reduce(function(pages, row) { + var initial = row.name.charAt(0); + + if(!pages[initial]) pages[initial] = []; + pages[initial].push(row); + + if(initials.indexOf(initial) < 0) + { + initials.push(initial); + initials.sort(); + } + + return pages; + + }, {})[initials[pageNumber - 1]] || []; + } + + } + ]); + + +
+
+
+
+
+ + .grid { + width: 600px; + } + +
diff --git a/misc/tutorial/407_custom_scrolling.ngdoc b/misc/tutorial/407_custom_scrolling.ngdoc new file mode 100644 index 0000000000..935b5635d6 --- /dev/null +++ b/misc/tutorial/407_custom_scrolling.ngdoc @@ -0,0 +1,59 @@ +@ngdoc overview +@name Tutorial: 407 Custom Scrolling +@description + + + +The custom scrolling feature takes over the default scrolling logic in order to ensure that grid scrolling works without a lag on devices. +To enable, you must include the 'ui.grid.customScrolling' module and you must include the ui-grid-custom-scrolling directive on your grid element. + +Documentation for the custom scrolling feature is provided in the api documentation, in particular: + +- {@link api/ui.grid.customScrolling.constant:uiGridScrollerConstants uiGridScrollerConstants} +- {@link api/ui.grid.customScrolling.service:uiGridScroller uiGridScroller} +- {@link api/ui.grid.customScrolling.directive:uiGridCustomScrolling uiGridCustomScrolling} + +@example + + + var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.pinning', 'ui.grid.customScrolling']); + + app.controller('MainCtrl', ['$scope', '$http', '$log', function ($scope, $http, $log) { + $scope.gridOptions = {}; + + $scope.gridOptions.columnDefs = [ + { name:'id', width:50, enablePinning:false }, + { name:'name', width:100, pinnedLeft:true }, + { name:'age', width:100, pinnedRight:true }, + { name:'address.street', width:150 }, + { name:'address.city', width:150 }, + { name:'address.state', width:50 }, + { name:'address.zip', width:50 }, + { name:'company', width:100 }, + { name:'email', width:100 }, + { name:'phone', width:200 }, + { name:'about', width:300 }, + { name:'friends[0].name', displayName:'1st friend', width:150 }, + { name:'friends[1].name', displayName:'2nd friend', width:150 }, + { name:'friends[2].name', displayName:'3rd friend', width:150 }, + ]; + + $http.get('/data/500_complex.json') + .success(function(data) { + $scope.gridOptions.data = data; + }); + }]); + + +
+
+
+
+ + .grid { + width: 100%; + height: 400px; + } + +
diff --git a/src/features/cellnav/js/cellnav.js b/src/features/cellnav/js/cellnav.js index fe1484a403..6a3fb5fa6e 100644 --- a/src/features/cellnav/js/cellnav.js +++ b/src/features/cellnav/js/cellnav.js @@ -128,7 +128,7 @@ var nextColIndex = curColIndex === 0 ? focusableCols.length - 1 : curColIndex - 1; //get column to left - if (nextColIndex > curColIndex) { + if (nextColIndex >= curColIndex) { // On the first row // if (curRowIndex === 0 && curColIndex === 0) { // return null; @@ -160,7 +160,7 @@ } var nextColIndex = curColIndex === focusableCols.length - 1 ? 0 : curColIndex + 1; - if (nextColIndex < curColIndex) { + if (nextColIndex <= curColIndex) { if (curRowIndex === focusableRows.length - 1) { return new GridRowColumn(curRow, focusableCols[nextColIndex]); //return same row } @@ -692,8 +692,8 @@ var newRowCol = new GridRowColumn(row, col); if (grid.cellNav.lastRowCol === null || grid.cellNav.lastRowCol.row !== newRowCol.row || grid.cellNav.lastRowCol.col !== newRowCol.col){ - grid.api.cellNav.raise.navigate(newRowCol, grid.cellNav.lastRowCol); - grid.cellNav.lastRowCol = newRowCol; + grid.api.cellNav.raise.navigate(newRowCol, grid.cellNav.lastRowCol, originEvt); + grid.cellNav.lastRowCol = newRowCol; } if (uiGridCtrl.grid.options.modifierKeysToMultiSelectCells && modifierDown) { grid.cellNav.focusedCells.push(rowCol); @@ -757,7 +757,7 @@ // Scroll to the new cell, if it's not completely visible within the render container's viewport grid.scrollToIfNecessary(rowCol.row, rowCol.col).then(function () { - uiGridCtrl.cellNav.broadcastCellNav(rowCol); + uiGridCtrl.cellNav.broadcastCellNav(rowCol, null, evt); }); diff --git a/src/features/custom-scrolling/js/custom-scrolling.js b/src/features/custom-scrolling/js/custom-scrolling.js new file mode 100644 index 0000000000..51f25c91cd --- /dev/null +++ b/src/features/custom-scrolling/js/custom-scrolling.js @@ -0,0 +1,415 @@ +(function() { + 'use strict'; + + /** + * @ngdoc overview + * @name ui.grid.customScrolling + * + * @description + * + * #ui.grid.customScrolling + * + * This module provides a custom grid scroller that works as an alternative to the native scroll event that + * uses touch events to ensure that grid scrolling works without a lag on devices. + * + */ + var module = angular.module('ui.grid.customScrolling', ['ui.grid']); + + /** + * @ngdoc object + * @name ui.grid.customScrolling.constant:uiGridScrollerConstants + * + * @description Constants for use with the uiGridScroller + */ + module.constant('uiGridScrollerConstants', { + /** + * @ngdoc object + * @name deceleration + * @propertyOf ui.grid.customScrolling.constant:uiGridScrollerConstants + * @description Used in {@link ui.grid.customScrolling.service:uiGridScroller#momentum uiGridScroller.momentum} + * to calculates current momentum of the scrolling. + */ + deceleration: 0.0007, + + /** + * @ngdoc object + * @name scrollType + * @propertyOf ui.grid.customScrolling.constant:uiGridScrollerConstants + * @description Used in {@link ui.grid.customScrolling.service:uiGridScroller uiGridScroller}, + * to the type of scroll event currently in progress + * + * Available options are: + * - `uiGridScrollerConstants.scrollEvent.NONE` - set when no scroll events are being triggered + * - `uiGridScrollerConstants.scrollEvent.TOUCHABLE` - set when touchstart, touchmove or touchend are triggered + * - `uiGridScrollerConstants.scrollEvent.MOUSE` - set when mousedown, mousemove or mouseup are triggered + * - `uiGridScrollerConstants.scrollEvent.POINTER` - set when pointerdown, pointermove or pointerup are triggered + */ + scrollType: { + NONE: 0, + TOUCHABLE: 1, + MOUSE: 2, + POINTER: 3 + } + }); + + /** + * @ngdoc service + * @name ui.grid.customScrolling.service:uiGridScroller + * @description uiGridScroller is an alternative to the native scroll event that uses touch events to ensure that grid scrolling works + * without a lag on devices. + * @param {object} element Element being scrolled + * @param {function} scrollHandler Function that needs to be called when scrolling happens. + */ + module.factory('uiGridScroller', ['$window', 'gridUtil', 'uiGridScrollerConstants', + function($window, gridUtil, uiGridScrollerConstants) { + var isAnimating; + + /** + * @ngdoc object + * @name initiated + * @propertyOf ui.grid.customScrolling.service:uiGridScroller + * @description Keeps track of which type of scrolling event has been initiated + * and sets it to NONE, when no event is being triggered. + */ + uiGridScroller.initiated = uiGridScrollerConstants.scrollType.NONE; + + function uiGridScroller(element, scrollHandler) { + var pointX, pointY, startTime, startX, startY, maxScroll, + scroller = element[0].children[0], + initType = { + touchstart: uiGridScrollerConstants.scrollType.TOUCHABLE, + touchmove: uiGridScrollerConstants.scrollType.TOUCHABLE, + touchend: uiGridScrollerConstants.scrollType.TOUCHABLE, + touchcancel: uiGridScrollerConstants.scrollType.TOUCHABLE + + // TODO: Enhance this scroller to support mouse and pointer events for better performance in slow machines + // mousedown: uiGridScrollerConstants.scrollType.MOUSE, + // mousemove: uiGridScrollerConstants.scrollType.MOUSE, + // mouseup: uiGridScrollerConstants.scrollType.MOUSE, + // + // pointerdown: uiGridScrollerConstants.scrollType.POINTER, + // pointermove: uiGridScrollerConstants.scrollType.POINTER, + // pointerup: uiGridScrollerConstants.scrollType.POINTER + }; + + if ('onmousedown' in $window) { + element.on('scroll', scrollHandler); + } + + if (gridUtil.isTouchEnabled()) { + element.on('touchstart', start); + element.on('touchmove', move); + element.on('touchcancel', end); + element.on('touchend', end); + } + + /** + * @ngdoc function + * @name start + * @methodOf ui.grid.customScrolling.service:uiGridScroller + * @description Gets the current coordinates and time, as well as the target coordinate + * and initializes the scrolling event + * @param {object} event The event object + */ + function start(event) { + var point = event.touches ? event.touches[0] : event; + + element.off('scroll', scrollHandler); + + uiGridScroller.initiated = initType[event.type]; + + pointX = point.pageX; + pointY = point.pageY; + + startTime = (new Date()).getTime(); + startX = element[0].scrollLeft; + startY = element[0].scrollTop; + isAnimating = false; + } + + /** + * @ngdoc function + * @name calcNewMove + * @methodOf ui.grid.customScrolling.service:uiGridScroller + * @description Calculates the next position of the element for a particular axis + * based on the delta. + * @param {number} scrollPos The original position of the element. + * @param {number} delta The amount the pointer moved. + * @param {number} axis The original position. + * @returns {number} The next position of the element. + */ + function calcNewMove(scrollPos, delta, axis) { + var newMove = scrollPos + delta; + + if (newMove < 0 || newMove > getMaxScroll()[axis]) { + newMove = newMove < 0 ? 0 : getMaxScroll()[axis]; + } + + return newMove; + } + + /** + * @ngdoc function + * @name move + * @methodOf ui.grid.customScrolling.service:uiGridScroller + * @description Calculates what the next move should be and starts the scrolling. + * @param {object} event The event object + */ + function move(event) { + event.preventDefault(); + + if (initType[event.type] !== uiGridScroller.initiated) { + return; + } + + var newX, newY, timestamp = (new Date()).getTime(), + point = event.touches ? event.touches[0] : event, + deltaX = pointX - point.pageX, + deltaY = pointY - point.pageY; + + pointX = point.pageX; + pointY = point.pageY; + + newX = calcNewMove(element[0].scrollLeft, deltaX, 'x'); + newY = calcNewMove(element[0].scrollTop, deltaY, 'y'); + + if (timestamp - startTime > 300) { + startTime = (new Date()).getTime(); + startX = newX; + startY = newY; + } + + translate(newX, newY, element); + + scrollHandler.call(null, event); + } + + /** + * @ngdoc function + * @name end + * @methodOf ui.grid.customScrolling.service:uiGridScroller + * @description Finishes the scrolling animation. + * @param {object} event The event object + */ + function end(event) { + if (initType[event.type] !== uiGridScroller.initiated) { + return; + } + + var duration = (new Date()).getTime() - startTime, + momentumX = momentum(element[0].scrollLeft, startX, duration), + momentumY = momentum(element[0].scrollTop, startY, duration), + newX = momentumX.destination, + newY = momentumY.destination, + time = Math.max(momentumX.duration, momentumY.duration); + + animate(newX, newY, time, element, scrollHandler.bind(null, event)); + + uiGridScroller.initiated = uiGridScrollerConstants.scrollType.NONE; + } + + /** + * @ngdoc function + * @name momentum + * @methodOf ui.grid.customScrolling.service:uiGridScroller + * @description Calculates current momentum of the scrolling based on the current position of the element, + * its initial position and the duration of this movement. + * @param {number} curr The current position of the element + * @param {number} start The original position of the element + * @param {number} time The time it has taken for the element to get to its current position. + * @returns {object} An object with the next position for the element and how long + * that animation should take. + */ + function momentum(curr, start, time) { + curr = Math.abs(curr); + start = Math.abs(start); + + var distance = curr - start, + speed = Math.abs(distance) / time, + destination = curr + (speed * speed) / (2 * uiGridScrollerConstants.deceleration) * (distance >= 0 ? 1 : -1), + duration = speed / uiGridScrollerConstants.deceleration; + + return { + destination: Math.round(destination), + duration: duration + }; + } + + /** + * @ngdoc function + * @name getMaxScroll + * @methodOf ui.grid.customScrolling.service:uiGridScroller + * @description Gets the limit of the scrolling for both the x and y positions. + * @returns {object} An object with the x and y scroll limits. + */ + function getMaxScroll() { + if (!maxScroll) { + maxScroll = { + x: scroller.offsetWidth - element[0].clientWidth, + y: scroller.offsetHeight - element[0].clientHeight + }; + } + return maxScroll; + } + } + + /** + * @ngdoc function + * @name translate + * @methodOf ui.grid.customScrolling.service:uiGridScroller + * @description Updates the element's scroll position. + * @param {number} x The horizontal position of the element + * @param {number} y The vertical position of the element + * @param {object} element The element being updated + */ + function translate(x, y, element) { + element[0].scrollLeft = x; + element[0].scrollTop = y; + } + + /** + * @ngdoc function + * @name easeClb + * @methodOf ui.grid.customScrolling.service:uiGridScroller + * @description Calculates the ease resolution base on the current animation times. + * @param {number} relPoint The time the animation is taking between frames. + * @returns {number} The ideal ease time. + */ + function easeClb(relPoint) { + return relPoint * ( 2 - relPoint ); + } + + /** + * @ngdoc function + * @name calcNewPos + * @methodOf ui.grid.customScrolling.service:uiGridScroller + * @description Calculates the new position of the element based on where it started, the animation time + * and where it is ultimately supposed to end up. + * @param {number} destPos The destination. + * @param {number} easeRes The ideal ease time. + * @param {number} startPos The original position. + * @returns {number} The next position of the element. + */ + function calcNewPos(destPos, easeRes, startPos) { + return ( destPos - Math.abs(startPos) ) * easeRes + Math.abs(startPos); + } + + /** + * @ngdoc function + * @name animate + * @methodOf ui.grid.customScrolling.service:uiGridScroller + * @description Calculates the ease resolution base on the current animation times. + * @param {number} destX The coordinate of the x axis that the scrolling needs to animate to. + * @param {number} destY The coordinate of the y axis that the scrolling needs to animate to. + * @param {number} duration The animation duration + * @param {object} element The element being updated + * @param {function} callback Function that needs to be called when the animation is done. + */ + function animate(destX, destY, duration, element, callback) { + var startTime = (new Date()).getTime(), + startX = element[0].scrollLeft, + startY = element[0].scrollTop, + destTime = startTime + duration; + + isAnimating = true; + + next(); + + function next() { + var now = (new Date()).getTime(), + relPoint, easeRes, newX, newY; + + if (now >= destTime) { + isAnimating = false; + translate(destX, destY, element); + element.on('scroll', callback); + return; + } + + relPoint = (now - startTime) / duration; + + easeRes = easeClb(relPoint); + + newX = calcNewPos(destX, easeRes, startX); + newY = calcNewPos(destY, easeRes, startY); + + translate(newX, newY, element); + + callback.call(); + + if (isAnimating) { + window.requestAnimationFrame(next); + } else { + element.on('scroll', callback); + } + } + } + + return uiGridScroller; + }]); + + /** + * @ngdoc directive + * @name ui.grid.customScrolling.directive:uiGridCustomScrolling + * @element div + * @restrict EA + * + * @description Updates the grid to use the gridScroller instead of the jquery scroll event + * + * @example + + + var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.pinning', 'ui.grid.customScrolling']); + + app.controller('MainCtrl', ['$scope', '$http', '$log', function ($scope, $http, $log) { + $scope.gridOptions = {}; + + $scope.gridOptions.columnDefs = [ + { name:'id', width:50, enablePinning:false }, + { name:'name', width:100, pinnedLeft:true }, + { name:'age', width:100, pinnedRight:true }, + { name:'address.street', width:150 }, + { name:'address.city', width:150 }, + { name:'address.state', width:50 }, + { name:'address.zip', width:50 }, + { name:'company', width:100 }, + { name:'email', width:100 }, + { name:'phone', width:200 }, + { name:'about', width:300 }, + { name:'friends[0].name', displayName:'1st friend', width:150 }, + { name:'friends[1].name', displayName:'2nd friend', width:150 }, + { name:'friends[2].name', displayName:'3rd friend', width:150 }, + ]; + + $http.get('/data/500_complex.json') + .success(function(data) { + $scope.gridOptions.data = data; + }); + }]); + + +
+
+
+
+
+ */ + module.directive('uiGridCustomScrolling', ['uiGridScroller', + function(uiGridScroller) { + return { + require: 'uiGrid', + scope: false, + compile: function() { + return { + pre: function($scope, $elm, $attrs, uiGridCtrl) { + // initializes custom scroller to be the gridScroller when options exist + if (uiGridCtrl.grid && uiGridCtrl.grid.options) { + uiGridCtrl.grid.options.customScroller = uiGridScroller; + } + }, + post: angular.noop + }; + } + }; + }]); +})(); diff --git a/src/features/custom-scrolling/test/ui-grid-custom-scrolling.directive.spec.js b/src/features/custom-scrolling/test/ui-grid-custom-scrolling.directive.spec.js new file mode 100644 index 0000000000..8384bc129a --- /dev/null +++ b/src/features/custom-scrolling/test/ui-grid-custom-scrolling.directive.spec.js @@ -0,0 +1,30 @@ +describe('ui.grid.customScrolling', function() { + describe('uiGridCustomScrolling Directive', function() { + var $compile, $rootScope, $scope, elm; + + beforeEach(function() { + module('ui.grid'); + module('ui.grid.customScrolling'); + + inject(function (_$rootScope_, _$compile_) { + $rootScope = _$rootScope_; + $compile = _$compile_; + }); + + $scope = $rootScope.$new(); + $scope.gridOpts = { + data: [{ name: 'Bob' }, {name: 'Mathias'}, {name: 'Fred'}] + }; + + elm = angular.element('
'); + + $compile(elm)($scope); + $scope.$digest(); + }); + + it('should update the grid options to define a customScroller', function() { + expect($scope.gridOpts.customScroller).toBeDefined(); + expect(angular.isFunction($scope.gridOpts.customScroller)).toBe(true); + }); + }); +}); diff --git a/src/features/custom-scrolling/test/ui-grid-scroller.factory.spec.js b/src/features/custom-scrolling/test/ui-grid-scroller.factory.spec.js new file mode 100644 index 0000000000..a6d4abd56b --- /dev/null +++ b/src/features/custom-scrolling/test/ui-grid-scroller.factory.spec.js @@ -0,0 +1,213 @@ +describe('ui.grid.customScrolling', function() { + describe('uiGridScroller', function() { + var element, scrollHandler, gridUtil, uiGridScroller, uiGridScrollerConstants; + + beforeEach(function() { + element = { + 0: { + children: { + 0: {} + } + }, + on: jasmine.createSpy('on'), + off: jasmine.createSpy('off') + }; + scrollHandler = jasmine.createSpy('scrollHandler'); + gridUtil = jasmine.createSpyObj('gridUtil', ['isTouchEnabled']); + + module('ui.grid.customScrolling', function($provide) { + $provide.value('gridUtil', gridUtil); + }); + + inject(function(_uiGridScroller_, _uiGridScrollerConstants_) { + uiGridScroller = _uiGridScroller_; + uiGridScrollerConstants = _uiGridScrollerConstants_; + }); + }); + + describe('when gridUtils.isTouchEnabled returns true', function() { + beforeEach(function() { + gridUtil.isTouchEnabled.and.returnValue(true); + uiGridScroller(element, scrollHandler); + }); + it('should initialize uiGridScroller.initiated to NONE', function() { + expect(uiGridScroller.initiated).toEqual(uiGridScrollerConstants.scrollType.NONE); + }); + describe('events', function() { + describe('on touchstart', function() { + beforeEach(function() { + element.on.and.callFake(function(eventName, callback) { + if (eventName === 'touchstart') { + callback({ + type: eventName, + touches: [true] + }); + } + }); + uiGridScroller(element, scrollHandler); + }); + it('should be initialized', function() { + expect(element.on).toHaveBeenCalledWith('touchstart', jasmine.any(Function)); + }); + it('should remove the scroll event from the element', function() { + expect(element.off).toHaveBeenCalledWith('scroll', scrollHandler); + }); + it('should update the uiGridScroller.initiated value to TOUCHABLE', function() { + expect(uiGridScroller.initiated).toEqual(uiGridScrollerConstants.scrollType.TOUCHABLE); + }); + afterEach(function() { + element.on.and.callFake(angular.noop); + }); + }); + describe('on touchmove', function() { + var preventDefaultSpy; + + beforeEach(function() { + preventDefaultSpy = jasmine.createSpy('preventDefault'); + element.on.and.callFake(function(eventName, callback) { + if (eventName === 'touchmove') { + callback({ + type: eventName, + touches: [true], + preventDefault: preventDefaultSpy + }); + } + }); + }); + it('should be initialized', function() { + expect(element.on).toHaveBeenCalledWith('touchmove', jasmine.any(Function)); + }); + describe('when the uiGridScroller has been initiated with a touch event', function() { + beforeEach(function() { + uiGridScroller.initiated = uiGridScrollerConstants.scrollType.TOUCHABLE; + uiGridScroller(element, scrollHandler); + }); + it('should prevent the default behavior', function() { + expect(preventDefaultSpy).toHaveBeenCalled(); + }); + it('should call the scrollHandler', function() { + expect(scrollHandler).toHaveBeenCalled(); + }); + }); + describe('when the uiGridScroller has not been initiated with a touch event', function() { + beforeEach(function() { + uiGridScroller.initiated = uiGridScrollerConstants.scrollType.NONE; + uiGridScroller(element, scrollHandler); + }); + it('should prevent the default behavior', function() { + expect(preventDefaultSpy).toHaveBeenCalled(); + }); + it('should not call the scrollHandler', function() { + expect(scrollHandler).not.toHaveBeenCalled(); + }); + }); + afterEach(function() { + element.on.and.callFake(angular.noop); + }); + }); + function testEndFunction() { + describe('when the uiGridScroller has been initiated with a touch event', function() { + beforeEach(function() { + uiGridScroller.initiated = uiGridScrollerConstants.scrollType.TOUCHABLE; + uiGridScroller(element, scrollHandler); + }); + it('should update the uiGridScroller.initiated value to NONE', function() { + expect(uiGridScroller.initiated).toEqual(uiGridScrollerConstants.scrollType.NONE); + }); + }); + describe('when the uiGridScroller has not been initiated with a touch event', function() { + beforeEach(function() { + uiGridScroller.initiated = uiGridScrollerConstants.scrollType.MOUSE; + uiGridScroller(element, scrollHandler); + }); + it('should not update the uiGridScroller.initiated value', function() { + expect(uiGridScroller.initiated).toEqual(uiGridScrollerConstants.scrollType.MOUSE); + }); + }); + afterEach(function() { + element.on.and.callFake(angular.noop); + }); + } + describe('on touchend', function() { + beforeEach(function() { + element.on.and.callFake(function(eventName, callback) { + if (eventName === 'touchend') { + callback({ + type: eventName, + touches: [true] + }); + } + }); + }); + it('should be initialized', function() { + expect(element.on).toHaveBeenCalledWith('touchend', jasmine.any(Function)); + }); + testEndFunction(); + }); + describe('on touchcancel', function() { + beforeEach(function() { + element.on.and.callFake(function(eventName, callback) { + if (eventName === 'touchcancel') { + callback({ + type: eventName, + touches: [true] + }); + } + }); + }); + it('should be initialized', function() { + expect(element.on).toHaveBeenCalledWith('touchcancel', jasmine.any(Function)); + }); + testEndFunction(); + }); + }); + afterEach(function() { + element.on.calls.reset(); + element.off.calls.reset(); + gridUtil.isTouchEnabled.calls.reset(); + }); + }); + + describe('when gridUtils.isTouchEnabled returns false', function() { + beforeEach(function() { + gridUtil.isTouchEnabled.and.returnValue(false); + uiGridScroller(element, scrollHandler); + }); + it('should initialize uiGridScroller.initiated to NONE', function() { + expect(uiGridScroller.initiated).toEqual(uiGridScrollerConstants.scrollType.NONE); + }); + describe('events', function() { + describe('on scroll', function() { + it('should be initialized', function() { + expect(element.on).toHaveBeenCalledWith('scroll', scrollHandler); + }); + }); + describe('on touchstart', function() { + it('should not be initialized', function() { + expect(element.on).not.toHaveBeenCalledWith('touchstart', jasmine.any(Function)); + }); + }); + describe('on touchmove', function() { + it('should not be initialized', function() { + expect(element.on).not.toHaveBeenCalledWith('touchmove', jasmine.any(Function)); + }); + }); + describe('on touchend', function() { + it('should not be initialized', function() { + expect(element.on).not.toHaveBeenCalledWith('touchend', jasmine.any(Function)); + }); + }); + describe('on touchcancel', function() { + it('should not be initialized', function() { + expect(element.on).not.toHaveBeenCalledWith('touchcancel', jasmine.any(Function)); + }); + }); + }); + afterEach(function() { + element.on.calls.reset(); + element.off.calls.reset(); + gridUtil.isTouchEnabled.calls.reset(); + }); + }); + }); +}); diff --git a/src/features/edit/js/gridEdit.js b/src/features/edit/js/gridEdit.js index 8c7df01879..deb1cf3b81 100644 --- a/src/features/edit/js/gridEdit.js +++ b/src/features/edit/js/gridEdit.js @@ -528,13 +528,13 @@ } }); - cellNavNavigateDereg = uiGridCtrl.grid.api.cellNav.on.navigate($scope, function (newRowCol, oldRowCol) { + cellNavNavigateDereg = uiGridCtrl.grid.api.cellNav.on.navigate($scope, function (newRowCol, oldRowCol, evt) { if ($scope.col.colDef.enableCellEditOnFocus) { // Don't begin edit if the cell hasn't changed if ((!oldRowCol || newRowCol.row !== oldRowCol.row || newRowCol.col !== oldRowCol.col) && newRowCol.row === $scope.row && newRowCol.col === $scope.col) { $timeout(function () { - beginEdit(); + beginEdit(evt); }); } } diff --git a/src/features/infinite-scroll/js/infinite-scroll.js b/src/features/infinite-scroll/js/infinite-scroll.js index ab68de973b..25307ef977 100644 --- a/src/features/infinite-scroll/js/infinite-scroll.js +++ b/src/features/infinite-scroll/js/infinite-scroll.js @@ -125,12 +125,11 @@ * infinite scroll events upward * @param {boolean} scrollDown flag that there are pages downwards, so * fire infinite scroll events downward - * @returns {promise} promise that is resolved when the scroll reset is complete */ resetScroll: function( scrollUp, scrollDown ) { service.setScrollDirections( grid, scrollUp, scrollDown); - return service.adjustInfiniteScrollPosition(grid, 0); + service.adjustInfiniteScrollPosition(grid, 0); }, @@ -414,7 +413,6 @@ * @description This function fires 'needLoadMoreData' or 'needLoadMoreDataTop' event based on scrollDirection * @param {Grid} grid the grid we're working on * @param {number} scrollTop the position through the grid that we want to scroll to - * @returns {promise} a promise that is resolved when the scrolling finishes */ adjustInfiniteScrollPosition: function (grid, scrollTop) { var scrollEvent = new ScrollEvent(grid, null, null, 'ui.grid.adjustInfiniteScrollPosition'), @@ -448,7 +446,6 @@ * infinite scroll events upward * @param {boolean} scrollDown flag that there are pages downwards, so * fire infinite scroll events downward - * @returns {promise} a promise that is resolved when the scrolling finishes */ dataRemovedTop: function( grid, scrollUp, scrollDown ) { var newVisibleRows, oldTop, newTop, rowHeight; @@ -462,7 +459,7 @@ // of rows removed newTop = oldTop - ( grid.infiniteScroll.previousVisibleRows - newVisibleRows )*rowHeight; - return service.adjustInfiniteScrollPosition( grid, newTop ); + service.adjustInfiniteScrollPosition( grid, newTop ); }, /** @@ -485,7 +482,7 @@ newTop = grid.infiniteScroll.prevScrollTop; - return service.adjustInfiniteScrollPosition( grid, newTop ); + service.adjustInfiniteScrollPosition( grid, newTop ); } }; return service; diff --git a/src/features/pagination/js/pagination.js b/src/features/pagination/js/pagination.js index c6ddecebce..8911abcd70 100644 --- a/src/features/pagination/js/pagination.js +++ b/src/features/pagination/js/pagination.js @@ -65,6 +65,32 @@ getPage: function () { return grid.options.enablePagination ? grid.options.paginationCurrentPage : null; }, + /** + * @ngdoc method + * @name getFirstRowIndex + * @methodOf ui.grid.pagination.api:PublicAPI + * @description Returns the index of the first row of the current page. + */ + getFirstRowIndex: function () { + if (grid.options.useCustomPagination) { + return grid.options.paginationPageSizes.reduce(function(result, size, index) { + return index < grid.options.paginationCurrentPage - 1 ? result + size : result; + }, 0); + } + return ((grid.options.paginationCurrentPage - 1) * grid.options.paginationPageSize); + }, + /** + * @ngdoc method + * @name getLastRowIndex + * @methodOf ui.grid.pagination.api:PublicAPI + * @description Returns the index of the last row of the current page. + */ + getLastRowIndex: function () { + if (grid.options.useCustomPagination) { + return publicApi.methods.pagination.getFirstRowIndex() + grid.options.paginationPageSizes[grid.options.paginationCurrentPage - 1]; + } + return Math.min(grid.options.paginationCurrentPage * grid.options.paginationPageSize, grid.options.totalItems); + }, /** * @ngdoc method * @name getTotalPages @@ -76,6 +102,10 @@ return null; } + if (grid.options.useCustomPagination) { + return grid.options.paginationPageSizes.length; + } + return (grid.options.totalItems === 0) ? 1 : Math.ceil(grid.options.totalItems / grid.options.paginationPageSize); }, /** @@ -146,12 +176,14 @@ var visibleRows = renderableRows.filter(function (row) { return row.visible; }); grid.options.totalItems = visibleRows.length; - var firstRow = (currentPage - 1) * pageSize; + var firstRow = publicApi.methods.pagination.getFirstRowIndex(); + var lastRow = publicApi.methods.pagination.getLastRowIndex(); + if (firstRow > visibleRows.length) { currentPage = grid.options.paginationCurrentPage = 1; firstRow = (currentPage - 1) * pageSize; } - return visibleRows.slice(firstRow, firstRow + pageSize); + return visibleRows.slice(firstRow, lastRow); }; grid.registerRowsProcessor(processPagination, 900 ); @@ -189,6 +221,16 @@ * and totalItems. Defaults to `false` */ gridOptions.useExternalPagination = gridOptions.useExternalPagination === true; + + /** + * @ngdoc property + * @name useCustomPagination + * @propertyOf ui.grid.pagination.api:GridOptions + * @description Disables client-side pagination. When true, handle the `paginationChanged` event and set `data`, + * `firstRowIndex`, `lastRowIndex`, and `totalItems`. Defaults to `false`. + */ + gridOptions.useCustomPagination = gridOptions.useCustomPagination === true; + /** * @ngdoc property * @name totalItems @@ -367,13 +409,6 @@ $scope.$on('$destroy', dataChangeDereg); - var setShowing = function () { - $scope.showingLow = ((options.paginationCurrentPage - 1) * options.paginationPageSize) + 1; - $scope.showingHigh = Math.min(options.paginationCurrentPage * options.paginationPageSize, options.totalItems); - }; - - var deregT = $scope.$watch('grid.options.totalItems + grid.options.paginationPageSize', setShowing); - var deregP = $scope.$watch('grid.options.paginationCurrentPage + grid.options.paginationPageSize', function (newValues, oldValues) { if (newValues === oldValues || oldValues === undefined) { return; @@ -389,30 +424,25 @@ return; } - setShowing(); uiGridPaginationService.onPaginationChanged($scope.grid, options.paginationCurrentPage, options.paginationPageSize); } ); $scope.$on('$destroy', function() { - deregT(); deregP(); }); $scope.cantPageForward = function () { - if (options.totalItems > 0) { - return options.paginationCurrentPage >= $scope.paginationApi.getTotalPages(); + if ($scope.paginationApi.getTotalPages()) { + return $scope.cantPageToLast(); } else { return options.data.length < 1; } }; $scope.cantPageToLast = function () { - if (options.totalItems > 0) { - return $scope.cantPageForward(); - } else { - return true; - } + var totalPages = $scope.paginationApi.getTotalPages(); + return !totalPages || options.paginationCurrentPage >= totalPages; }; $scope.cantPageBackward = function () { diff --git a/src/features/pagination/templates/pagination.html b/src/features/pagination/templates/pagination.html index c867c5edcb..630e1f86a9 100644 --- a/src/features/pagination/templates/pagination.html +++ b/src/features/pagination/templates/pagination.html @@ -78,7 +78,7 @@
+ ng-if="grid.options.paginationPageSizes.length > 1 && !grid.options.useCustomPagination">