Skip to content

Instantly share code, notes, and snippets.

@frederickjansen
Last active May 4, 2017 04:43
Show Gist options
  • Save frederickjansen/47805103e6b87e90b7a1 to your computer and use it in GitHub Desktop.
Save frederickjansen/47805103e6b87e90b7a1 to your computer and use it in GitHub Desktop.
Reverse infinite scroll for Ionic
/**
* @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(); });
}
}
};
}]);
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