Last active
May 4, 2017 04:43
-
-
Save frederickjansen/47805103e6b87e90b7a1 to your computer and use it in GitHub Desktop.
Reverse infinite scroll for Ionic
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @ngdoc directive | |
* @name ionInfiniteScrollReverse | |
* @module ionic | |
* @parent ionic.directive:ionContent, ionic.directive:ionScroll | |
* @restrict E | |
* | |
* @description | |
* The ionInfiniteScrollReverse directive allows you to call a function whenever | |
* the user gets to the bottom of the page or near the bottom of the page. | |
* | |
* The expression you pass in for `on-infinite` is called when the user scrolls | |
* greater than `distance` away from the bottom of the content. Once `on-infinite` | |
* is done loading new data, it should broadcast the `scroll.infiniteScrollComplete` | |
* event from your controller (see below example). | |
* | |
* @param {expression} on-infinite What to call when the scroller reaches the | |
* bottom. | |
* @param {string=} distance The distance from the bottom that the scroll must | |
* reach to trigger the on-infinite expression. This can be either a percentage | |
* or the number of pixels. Default: 2.5%. | |
* @param {string=} spinner The {@link ionic.directive:ionSpinner} to show while loading. The SVG | |
* {@link ionic.directive:ionSpinner} is now the default, replacing rotating font icons. | |
* @param {string=} icon The icon to show while loading. Default: 'ion-load-d'. This is depreicated | |
* in favor of the SVG {@link ionic.directive:ionSpinner}. | |
* @param {boolean=} immediate-check Whether to check the infinite scroll bounds immediately on load. | |
* @param {boolean=} reverse Whether to reverse the infinite scroller trigger from right/bottom to left/top. | |
* | |
* @usage | |
* ```html | |
* <ion-content ng-controller="MyController"> | |
* <ion-list> | |
* .... | |
* .... | |
* </ion-list> | |
* | |
* <ion-infinite-scroll-reverse | |
* on-infinite="loadMore()" | |
* distance="2.5%" | |
* reverse="true"> | |
* </ion-infinite-scroll-reverse> | |
* </ion-content> | |
* ``` | |
* ```js | |
* function MyController($scope, $http) { | |
* $scope.items = []; | |
* $scope.loadMore = function() { | |
* $http.get('/more-items').success(function(items) { | |
* useItems(items); | |
* $scope.$broadcast('scroll.infiniteScrollComplete'); | |
* }); | |
* }; | |
* | |
* $scope.$on('$stateChangeSuccess', function() { | |
* $scope.loadMore(); | |
* }); | |
* } | |
* ``` | |
* | |
* An easy to way to stop infinite scroll once there is no more data to load | |
* is to use angular's `ng-if` directive: | |
* | |
* ```html | |
* <ion-infinite-scroll-reverse | |
* ng-if="moreDataCanBeLoaded()" | |
* icon="ion-loading-c" | |
* on-infinite="loadMoreData()" | |
* reverse="true"> | |
* </ion-infinite-scroll-reverse> | |
* ``` | |
*/ | |
angular.module('ionic') | |
.directive('ionInfiniteScrollReverse', ['$timeout', function($timeout) { | |
return { | |
restrict: 'E', | |
require: ['?^$ionicScroll', 'ionInfiniteScrollReverse'], | |
template: function($element, $attrs) { | |
if ($attrs.icon) return '<i class="icon {{icon()}} icon-refreshing {{scrollingType}}"></i>'; | |
return '<ion-spinner icon="{{spinner()}}"></ion-spinner>'; | |
}, | |
scope: true, | |
controller: '$ionInfiniteScrollReverse', | |
link: function($scope, $element, $attrs, ctrls) { | |
var infiniteScrollCtrl = ctrls[1]; | |
var scrollCtrl = infiniteScrollCtrl.scrollCtrl = ctrls[0]; | |
var jsScrolling = infiniteScrollCtrl.jsScrolling = !scrollCtrl.isNative(); | |
// if this view is not beneath a scrollCtrl, it can't be injected, proceed w/ native scrolling | |
if (jsScrolling) { | |
infiniteScrollCtrl.scrollView = scrollCtrl.scrollView; | |
$scope.scrollingType = 'js-scrolling'; | |
//bind to JS scroll events | |
scrollCtrl.$element.on('scroll', infiniteScrollCtrl.checkBounds); | |
} else { | |
// grabbing the scrollable element, to determine dimensions, and current scroll pos | |
var scrollEl = ionic.DomUtil.getParentOrSelfWithClass($element[0].parentNode, 'overflow-scroll'); | |
infiniteScrollCtrl.scrollEl = scrollEl; | |
// if there's no scroll controller, and no overflow scroll div, infinite scroll wont work | |
if (!scrollEl) { | |
throw 'Infinite scroll must be used inside a scrollable div'; | |
} | |
//bind to native scroll events | |
infiniteScrollCtrl.scrollEl.addEventListener('scroll', infiniteScrollCtrl.checkBounds); | |
} | |
// Optionally check bounds on start after scrollView is fully rendered | |
var doImmediateCheck = angular.isDefined($attrs.immediateCheck) ? $scope.$eval($attrs.immediateCheck) : true; | |
if (doImmediateCheck) { | |
$timeout(function() { infiniteScrollCtrl.checkBounds(); }); | |
} | |
} | |
}; | |
}]); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
angular.module('ionic') | |
.controller('$ionInfiniteScrollReverse', [ | |
'$scope', | |
'$attrs', | |
'$element', | |
'$timeout', | |
function($scope, $attrs, $element, $timeout) { | |
var self = this; | |
self.isLoading = false; | |
$scope.icon = function() { | |
return angular.isDefined($attrs.icon) ? $attrs.icon : 'ion-load-d'; | |
}; | |
$scope.spinner = function() { | |
return angular.isDefined($attrs.spinner) ? $attrs.spinner : ''; | |
}; | |
$scope.$on('scroll.infiniteScrollComplete', function() { | |
finishInfiniteScroll(); | |
}); | |
$scope.$on('$destroy', function() { | |
if (self.scrollCtrl && self.scrollCtrl.$element) self.scrollCtrl.$element.off('scroll', self.checkBounds); | |
if (self.scrollEl && self.scrollEl.removeEventListener) { | |
self.scrollEl.removeEventListener('scroll', self.checkBounds); | |
} | |
}); | |
// debounce checking infinite scroll events | |
self.checkBounds = ionic.Utils.throttle(checkInfiniteBounds, 300); | |
function onInfinite() { | |
ionic.requestAnimationFrame(function() { | |
$element[0].classList.add('active'); | |
}); | |
self.isLoading = true; | |
$scope.$parent && $scope.$parent.$apply($attrs.onInfinite || ''); | |
} | |
function finishInfiniteScroll() { | |
ionic.requestAnimationFrame(function() { | |
$element[0].classList.remove('active'); | |
}); | |
$timeout(function() { | |
if (self.jsScrolling) self.scrollView.resize(); | |
// only check bounds again immediately if the page isn't cached (scroll el has height) | |
if ((self.jsScrolling && self.scrollView.__container && self.scrollView.__container.offsetHeight > 0) || | |
!self.jsScrolling) { | |
self.checkBounds(); | |
} | |
}, 30, false); | |
self.isLoading = false; | |
} | |
// check if we've scrolled far enough to trigger an infinite scroll | |
function checkInfiniteBounds() { | |
if (self.isLoading) return; | |
var maxScroll = {}; | |
if (self.jsScrolling) { | |
maxScroll = self.getJSMaxScroll(); | |
var scrollValues = self.scrollView.getValues(); | |
if ($attrs.reverse) { | |
if ((maxScroll.left !== -1 && scrollValues.left <= maxScroll.left) || | |
(maxScroll.top !== -1 && scrollValues.top <= maxScroll.top)) { | |
onInfinite(); | |
} | |
} else { | |
if ((maxScroll.left !== -1 && scrollValues.left >= maxScroll.left) || | |
(maxScroll.top !== -1 && scrollValues.top >= maxScroll.top)) { | |
onInfinite(); | |
} | |
} | |
} else { | |
maxScroll = self.getNativeMaxScroll(); | |
if ($attrs.reverse) { | |
if (( | |
maxScroll.left !== -1 && | |
self.scrollEl.scrollLeft <= maxScroll.left | |
) || ( | |
maxScroll.top !== -1 && | |
self.scrollEl.scrollTop <= maxScroll.top | |
)) { | |
onInfinite(); | |
} | |
} else { | |
if (( | |
maxScroll.left !== -1 && | |
self.scrollEl.scrollLeft >= maxScroll.left - self.scrollEl.clientWidth | |
) || ( | |
maxScroll.top !== -1 && | |
self.scrollEl.scrollTop >= maxScroll.top - self.scrollEl.clientHeight | |
)) { | |
onInfinite(); | |
} | |
} | |
} | |
} | |
// determine the threshold at which we should fire an infinite scroll | |
// note: this gets processed every scroll event, can it be cached? | |
self.getJSMaxScroll = function() { | |
var maxValues = self.scrollView.getScrollMax(); | |
return { | |
left: self.scrollView.options.scrollingX ? | |
calculateMaxValue(maxValues.left) : | |
-1, | |
top: self.scrollView.options.scrollingY ? | |
calculateMaxValue(maxValues.top) : | |
-1 | |
}; | |
}; | |
self.getNativeMaxScroll = function() { | |
var maxValues = { | |
left: self.scrollEl.scrollWidth, | |
top: self.scrollEl.scrollHeight | |
}; | |
var computedStyle = window.getComputedStyle(self.scrollEl) || {}; | |
return { | |
left: maxValues.left && | |
(computedStyle.overflowX === 'scroll' || | |
computedStyle.overflowX === 'auto' || | |
self.scrollEl.style['overflow-x'] === 'scroll') ? | |
calculateMaxValue(maxValues.left) : -1, | |
top: maxValues.top && | |
(computedStyle.overflowY === 'scroll' || | |
computedStyle.overflowY === 'auto' || | |
self.scrollEl.style['overflow-y'] === 'scroll' ) ? | |
calculateMaxValue(maxValues.top) : -1 | |
}; | |
}; | |
// determine pixel refresh distance based on % or value | |
function calculateMaxValue(maximum) { | |
var distance = ($attrs.distance || '2.5%').trim(); | |
var isPercent = distance.indexOf('%') !== -1; | |
if ($attrs.reverse) { | |
return isPercent ? | |
maximum - (maximum * (1 - parseFloat(distance) / 100)) : | |
parseFloat(distance); | |
} else { | |
return isPercent ? | |
maximum * (1 - parseFloat(distance) / 100) : | |
maximum - parseFloat(distance); | |
} | |
} | |
//for testing | |
self.__finishInfiniteScroll = finishInfiniteScroll; | |
}]); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment