Skip to content

Commit

Permalink
feat(scrolling): Allow native scrolling to be configurable, add infin…
Browse files Browse the repository at this point in the history
…ite scroll support for native scrolling
  • Loading branch information
perrygovier committed Jan 14, 2015
1 parent d24ac30 commit 54c27ff
Show file tree
Hide file tree
Showing 7 changed files with 305 additions and 112 deletions.
115 changes: 115 additions & 0 deletions js/angular/controller/infiniteScrollController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
IonicModule
.controller('$ionInfiniteScroll', [
'$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-loading-d';
};

$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();
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 ((maxScroll.left !== -1 && scrollValues.left >= maxScroll.left) ||
(maxScroll.top !== -1 && scrollValues.top >= maxScroll.top)) {
onInfinite();
}
} else {
maxScroll = self.getNativeMaxScroll();
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: computedStyle.overflowX === 'scroll' ||
computedStyle.overflowX === 'auto' ||
self.scrollEl.style['overflow-x'] === 'scroll' ?
calculateMaxValue(maxValues.left) : -1,
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) {
distance = ($attrs.distance || '2.5%').trim();
isPercent = distance.indexOf('%') !== -1;
return isPercent ?
maximum * (1 - parseFloat(distance) / 100) :
maximum - parseFloat(distance);
}

}]);
6 changes: 4 additions & 2 deletions js/angular/directive/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ IonicModule
'$timeout',
'$controller',
'$ionicBind',
function($timeout, $controller, $ionicBind) {
'$ionicConfig',
function($timeout, $controller, $ionicBind, $ionicConfig) {
return {
restrict: 'E',
require: '^?ionNavView',
Expand Down Expand Up @@ -108,7 +109,8 @@ function($timeout, $controller, $ionicBind) {

if ($attr.scroll === "false") {
//do nothing
} else if(attr.overflowScroll === "true") {
} else if (attr.overflowScroll === "true" || !$ionicConfig.scrolling.jsScrolling()) {
// use native scrolling
$element.addClass('overflow-scroll');
} else {
var scrollViewOptions = {
Expand Down
101 changes: 29 additions & 72 deletions js/angular/directive/infiniteScroll.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* @param {string=} distance The distance from the bottom that the scroll must
* reach to trigger the on-infinite expression. Default: 1%.
* @param {string=} icon The icon to show while loading. Default: 'ion-loading-d'.
* @param {boolean=} immediate-check Whether to check the infinite scroll bounds immediately on load.
*
* @usage
* ```html
Expand Down Expand Up @@ -63,84 +64,40 @@
*/
IonicModule
.directive('ionInfiniteScroll', ['$timeout', function($timeout) {
function calculateMaxValue(distance, maximum, isPercent) {
return isPercent ?
maximum * (1 - parseFloat(distance,10) / 100) :
maximum - parseFloat(distance, 10);
}
return {
restrict: 'E',
require: ['^$ionicScroll', 'ionInfiniteScroll'],
template: '<i class="icon {{icon()}} icon-refreshing"></i>',
scope: {
load: '&onInfinite'
},
controller: ['$scope', '$attrs', function($scope, $attrs) {
this.isLoading = false;
this.scrollView = null; //given by link function
this.getMaxScroll = function() {
var distance = ($attrs.distance || '2.5%').trim();
var isPercent = distance.indexOf('%') !== -1;
var maxValues = this.scrollView.getScrollMax();
return {
left: this.scrollView.options.scrollingX ?
calculateMaxValue(distance, maxValues.left, isPercent) :
-1,
top: this.scrollView.options.scrollingY ?
calculateMaxValue(distance, maxValues.top, isPercent) :
-1
};
};
}],
require: ['?^$ionicScroll', 'ionInfiniteScroll'],
template: '<i class="icon {{icon()}} icon-refreshing {{scrollingType}}"></i>',
scope: true,
controller: '$ionInfiniteScroll',
link: function($scope, $element, $attrs, ctrls) {
var scrollCtrl = ctrls[0];
var infiniteScrollCtrl = ctrls[1];
var scrollView = infiniteScrollCtrl.scrollView = scrollCtrl.scrollView;

$scope.icon = function() {
return angular.isDefined($attrs.icon) ? $attrs.icon : 'ion-loading-d';
};

var onInfinite = function() {
$element[0].classList.add('active');
infiniteScrollCtrl.isLoading = true;
$scope.load();
};

var finishInfiniteScroll = function() {
$element[0].classList.remove('active');
$timeout(function() {
scrollView.resize();
checkBounds();
}, 0, false);
infiniteScrollCtrl.isLoading = false;
};

$scope.$on('scroll.infiniteScrollComplete', function() {
finishInfiniteScroll();
});

$scope.$on('$destroy', function() {
if(scrollCtrl && scrollCtrl.$element)scrollCtrl.$element.off('scroll', checkBounds);
});

var checkBounds = ionic.animationFrameThrottle(checkInfiniteBounds);

//Check bounds on start, after scrollView is fully rendered
$timeout(checkBounds, 0, false);
scrollCtrl.$element.on('scroll', checkBounds);

function checkInfiniteBounds() {
if (infiniteScrollCtrl.isLoading) return;

var scrollValues = scrollView.getValues();
var maxScroll = infiniteScrollCtrl.getMaxScroll();

if ((maxScroll.left !== -1 && scrollValues.left >= maxScroll.left) ||
(maxScroll.top !== -1 && scrollValues.top >= maxScroll.top)) {
onInfinite();
var scrollCtrl = infiniteScrollCtrl.scrollCtrl = ctrls[0];
var jsScrolling = infiniteScrollCtrl.jsScrolling = !!scrollCtrl;
// if this view is not beneath a scrollCtrl, it can't be injected, proceed w/ native scrolling
if (jsScrolling) {
infiniteScrollCtrl.scrollView = scrollCtrl.scrollView;
} 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 appropriate scroll event
if (jsScrolling) {
$scope.scrollingType = 'js-scrolling';
scrollCtrl.$element.on('scroll', infiniteScrollCtrl.checkBounds);
} else {
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(); });
}
}
};
}]);
7 changes: 7 additions & 0 deletions js/angular/service/ionicConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,9 @@ IonicModule
form: {
checkbox: PLATFORM
},
scrolling: {
jsScrolling: PLATFORM
},
tabs: {
style: PLATFORM,
position: PLATFORM
Expand Down Expand Up @@ -262,6 +265,10 @@ IonicModule
checkbox: 'circle'
},

scrolling: {
jsScrolling: true
},

tabs: {
style: 'standard',
position: 'bottom'
Expand Down
20 changes: 16 additions & 4 deletions scss/_scaffolding.scss
Original file line number Diff line number Diff line change
Expand Up @@ -261,25 +261,37 @@ body.grade-c {
ion-infinite-scroll {
height: 60px;
width: 100%;
opacity: 0;

display: block;

@include transition(opacity 0.25s);
// @include transition(opacity 0.25s);
@include display-flex();
@include flex-direction(row);
@include justify-content(center);
@include align-items(center);

.icon {

color: #666666;
font-size: 30px;
color: $scroll-refresh-icon-color;
&:before{
-webkit-transform: translate3d(0,0,0);
transform: translate3d(0,0,0);
}
}
&:not(.active) .icon:before{
-webkit-transform: translate3d(-1000px,0,0);
transform: translate3d(-1000px,0,0);

&.active {
opacity: 1;
}
}
// removing the animation when the spinner isn't shown
// this breaks up animations on iOS, so they are left with unnecessary reflows
body:not(.platform-ios) ion-infinite-scroll:not(.active) .icon{
-webkit-animation: none;
animation:none;
}

.overflow-scroll {
overflow-x: hidden;
Expand Down
11 changes: 9 additions & 2 deletions test/unit/angular/directive/content.unit.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
describe('Ionic Content directive', function() {
var compile, scope, timeout, window;
var compile, scope, timeout, window, ionicConfig;

beforeEach(module('ionic'));

beforeEach(inject(function($compile, $rootScope, $timeout, $window) {
beforeEach(inject(function($compile, $rootScope, $timeout, $window, $ionicConfig) {
compile = $compile;
scope = $rootScope;
timeout = $timeout;
window = $window;
ionicConfig = $ionicConfig;
ionic.Platform.setPlatform('Android');
}));

Expand Down Expand Up @@ -128,6 +129,12 @@ describe('Ionic Content directive', function() {
expect(vals.top).toBe(300);
});

it('Should allow native scrolling to be set by $ionicConfig ', function() {
ionicConfig.scrolling.jsScrolling(false);
var element = compile('<ion-content></ion-content>')(scope);
expect(element.hasClass('overflow-scroll')).toBe(true);
});

});
/* Tests #555, #1155 */
describe('Ionic Content Directive scoping', function() {
Expand Down
Loading

4 comments on commit 54c27ff

@apavillet
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This + Crosswalk = win

@perrygovier
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:)

@benjaminbertin
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great news!
Do you plan to have all of the JS scrolling functionnalities work with Android native scrolling (like collection-repeat)?

@srameshr
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@perrygovier Thanks! Infinite scrolling is available for overflow / native scrolling. I had a really bad hacky solution previously!

Please sign in to comment.