Skip to content

Commit 54c27ff

Browse files
committed
feat(scrolling): Allow native scrolling to be configurable, add infinite scroll support for native scrolling
1 parent d24ac30 commit 54c27ff

File tree

7 files changed

+305
-112
lines changed

7 files changed

+305
-112
lines changed

Diff for: js/angular/controller/infiniteScrollController.js

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
IonicModule
2+
.controller('$ionInfiniteScroll', [
3+
'$scope',
4+
'$attrs',
5+
'$element',
6+
'$timeout',
7+
function($scope, $attrs, $element, $timeout) {
8+
var self = this;
9+
self.isLoading = false;
10+
11+
$scope.icon = function() {
12+
return angular.isDefined($attrs.icon) ? $attrs.icon : 'ion-loading-d';
13+
};
14+
15+
$scope.$on('scroll.infiniteScrollComplete', function() {
16+
finishInfiniteScroll();
17+
});
18+
19+
$scope.$on('$destroy', function() {
20+
if (self.scrollCtrl && self.scrollCtrl.$element) self.scrollCtrl.$element.off('scroll', self.checkBounds);
21+
if (self.scrollEl && self.scrollEl.removeEventListener) {
22+
self.scrollEl.removeEventListener('scroll', self.checkBounds);
23+
}
24+
});
25+
26+
// debounce checking infinite scroll events
27+
self.checkBounds = ionic.Utils.throttle(checkInfiniteBounds, 300);
28+
29+
function onInfinite() {
30+
ionic.requestAnimationFrame(function() {
31+
$element[0].classList.add('active');
32+
});
33+
self.isLoading = true;
34+
$scope.$parent && $scope.$parent.$apply($attrs.onInfinite || '');
35+
}
36+
37+
function finishInfiniteScroll() {
38+
ionic.requestAnimationFrame(function() {
39+
$element[0].classList.remove('active');
40+
});
41+
$timeout(function() {
42+
if (self.jsScrolling) self.scrollView.resize();
43+
self.checkBounds();
44+
}, 30, false);
45+
self.isLoading = false;
46+
}
47+
48+
// check if we've scrolled far enough to trigger an infinite scroll
49+
function checkInfiniteBounds() {
50+
if (self.isLoading) return;
51+
var maxScroll = {};
52+
53+
if (self.jsScrolling) {
54+
maxScroll = self.getJSMaxScroll();
55+
var scrollValues = self.scrollView.getValues();
56+
if ((maxScroll.left !== -1 && scrollValues.left >= maxScroll.left) ||
57+
(maxScroll.top !== -1 && scrollValues.top >= maxScroll.top)) {
58+
onInfinite();
59+
}
60+
} else {
61+
maxScroll = self.getNativeMaxScroll();
62+
if ((
63+
maxScroll.left !== -1 &&
64+
self.scrollEl.scrollLeft >= maxScroll.left - self.scrollEl.clientWidth
65+
) || (
66+
maxScroll.top !== -1 &&
67+
self.scrollEl.scrollTop >= maxScroll.top - self.scrollEl.clientHeight
68+
)) {
69+
onInfinite();
70+
}
71+
}
72+
}
73+
74+
// determine the threshold at which we should fire an infinite scroll
75+
// note: this gets processed every scroll event, can it be cached?
76+
self.getJSMaxScroll = function() {
77+
var maxValues = self.scrollView.getScrollMax();
78+
return {
79+
left: self.scrollView.options.scrollingX ?
80+
calculateMaxValue(maxValues.left) :
81+
-1,
82+
top: self.scrollView.options.scrollingY ?
83+
calculateMaxValue(maxValues.top) :
84+
-1
85+
};
86+
};
87+
88+
self.getNativeMaxScroll = function() {
89+
var maxValues = {
90+
left: self.scrollEl.scrollWidth,
91+
top: self.scrollEl.scrollHeight
92+
};
93+
var computedStyle = window.getComputedStyle(self.scrollEl) || {};
94+
return {
95+
left: computedStyle.overflowX === 'scroll' ||
96+
computedStyle.overflowX === 'auto' ||
97+
self.scrollEl.style['overflow-x'] === 'scroll' ?
98+
calculateMaxValue(maxValues.left) : -1,
99+
top: computedStyle.overflowY === 'scroll' ||
100+
computedStyle.overflowY === 'auto' ||
101+
self.scrollEl.style['overflow-y'] === 'scroll' ?
102+
calculateMaxValue(maxValues.top) : -1
103+
};
104+
};
105+
106+
// determine pixel refresh distance based on % or value
107+
function calculateMaxValue(maximum) {
108+
distance = ($attrs.distance || '2.5%').trim();
109+
isPercent = distance.indexOf('%') !== -1;
110+
return isPercent ?
111+
maximum * (1 - parseFloat(distance) / 100) :
112+
maximum - parseFloat(distance);
113+
}
114+
115+
}]);

Diff for: js/angular/directive/content.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ IonicModule
4545
'$timeout',
4646
'$controller',
4747
'$ionicBind',
48-
function($timeout, $controller, $ionicBind) {
48+
'$ionicConfig',
49+
function($timeout, $controller, $ionicBind, $ionicConfig) {
4950
return {
5051
restrict: 'E',
5152
require: '^?ionNavView',
@@ -108,7 +109,8 @@ function($timeout, $controller, $ionicBind) {
108109

109110
if ($attr.scroll === "false") {
110111
//do nothing
111-
} else if(attr.overflowScroll === "true") {
112+
} else if (attr.overflowScroll === "true" || !$ionicConfig.scrolling.jsScrolling()) {
113+
// use native scrolling
112114
$element.addClass('overflow-scroll');
113115
} else {
114116
var scrollViewOptions = {

Diff for: js/angular/directive/infiniteScroll.js

+29-72
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* @param {string=} distance The distance from the bottom that the scroll must
2020
* reach to trigger the on-infinite expression. Default: 1%.
2121
* @param {string=} icon The icon to show while loading. Default: 'ion-loading-d'.
22+
* @param {boolean=} immediate-check Whether to check the infinite scroll bounds immediately on load.
2223
*
2324
* @usage
2425
* ```html
@@ -63,84 +64,40 @@
6364
*/
6465
IonicModule
6566
.directive('ionInfiniteScroll', ['$timeout', function($timeout) {
66-
function calculateMaxValue(distance, maximum, isPercent) {
67-
return isPercent ?
68-
maximum * (1 - parseFloat(distance,10) / 100) :
69-
maximum - parseFloat(distance, 10);
70-
}
7167
return {
7268
restrict: 'E',
73-
require: ['^$ionicScroll', 'ionInfiniteScroll'],
74-
template: '<i class="icon {{icon()}} icon-refreshing"></i>',
75-
scope: {
76-
load: '&onInfinite'
77-
},
78-
controller: ['$scope', '$attrs', function($scope, $attrs) {
79-
this.isLoading = false;
80-
this.scrollView = null; //given by link function
81-
this.getMaxScroll = function() {
82-
var distance = ($attrs.distance || '2.5%').trim();
83-
var isPercent = distance.indexOf('%') !== -1;
84-
var maxValues = this.scrollView.getScrollMax();
85-
return {
86-
left: this.scrollView.options.scrollingX ?
87-
calculateMaxValue(distance, maxValues.left, isPercent) :
88-
-1,
89-
top: this.scrollView.options.scrollingY ?
90-
calculateMaxValue(distance, maxValues.top, isPercent) :
91-
-1
92-
};
93-
};
94-
}],
69+
require: ['?^$ionicScroll', 'ionInfiniteScroll'],
70+
template: '<i class="icon {{icon()}} icon-refreshing {{scrollingType}}"></i>',
71+
scope: true,
72+
controller: '$ionInfiniteScroll',
9573
link: function($scope, $element, $attrs, ctrls) {
96-
var scrollCtrl = ctrls[0];
9774
var infiniteScrollCtrl = ctrls[1];
98-
var scrollView = infiniteScrollCtrl.scrollView = scrollCtrl.scrollView;
99-
100-
$scope.icon = function() {
101-
return angular.isDefined($attrs.icon) ? $attrs.icon : 'ion-loading-d';
102-
};
103-
104-
var onInfinite = function() {
105-
$element[0].classList.add('active');
106-
infiniteScrollCtrl.isLoading = true;
107-
$scope.load();
108-
};
109-
110-
var finishInfiniteScroll = function() {
111-
$element[0].classList.remove('active');
112-
$timeout(function() {
113-
scrollView.resize();
114-
checkBounds();
115-
}, 0, false);
116-
infiniteScrollCtrl.isLoading = false;
117-
};
118-
119-
$scope.$on('scroll.infiniteScrollComplete', function() {
120-
finishInfiniteScroll();
121-
});
122-
123-
$scope.$on('$destroy', function() {
124-
if(scrollCtrl && scrollCtrl.$element)scrollCtrl.$element.off('scroll', checkBounds);
125-
});
126-
127-
var checkBounds = ionic.animationFrameThrottle(checkInfiniteBounds);
128-
129-
//Check bounds on start, after scrollView is fully rendered
130-
$timeout(checkBounds, 0, false);
131-
scrollCtrl.$element.on('scroll', checkBounds);
132-
133-
function checkInfiniteBounds() {
134-
if (infiniteScrollCtrl.isLoading) return;
135-
136-
var scrollValues = scrollView.getValues();
137-
var maxScroll = infiniteScrollCtrl.getMaxScroll();
138-
139-
if ((maxScroll.left !== -1 && scrollValues.left >= maxScroll.left) ||
140-
(maxScroll.top !== -1 && scrollValues.top >= maxScroll.top)) {
141-
onInfinite();
75+
var scrollCtrl = infiniteScrollCtrl.scrollCtrl = ctrls[0];
76+
var jsScrolling = infiniteScrollCtrl.jsScrolling = !!scrollCtrl;
77+
// if this view is not beneath a scrollCtrl, it can't be injected, proceed w/ native scrolling
78+
if (jsScrolling) {
79+
infiniteScrollCtrl.scrollView = scrollCtrl.scrollView;
80+
} else {
81+
// grabbing the scrollable element, to determine dimensions, and current scroll pos
82+
var scrollEl = ionic.DomUtil.getParentOrSelfWithClass($element[0].parentNode,'overflow-scroll');
83+
infiniteScrollCtrl.scrollEl = scrollEl;
84+
// if there's no scroll controller, and no overflow scroll div, infinite scroll wont work
85+
if (!scrollEl) {
86+
throw 'Infinite scroll must be used inside a scrollable div';
14287
}
14388
}
89+
//bind to appropriate scroll event
90+
if (jsScrolling) {
91+
$scope.scrollingType = 'js-scrolling';
92+
scrollCtrl.$element.on('scroll', infiniteScrollCtrl.checkBounds);
93+
} else {
94+
infiniteScrollCtrl.scrollEl.addEventListener('scroll', infiniteScrollCtrl.checkBounds);
95+
}
96+
// Optionally check bounds on start after scrollView is fully rendered
97+
var doImmediateCheck = angular.isDefined($attrs.immediateCheck) ? $scope.$eval($attrs.immediateCheck) : true;
98+
if (doImmediateCheck) {
99+
$timeout(function() { infiniteScrollCtrl.checkBounds(); });
100+
}
144101
}
145102
};
146103
}]);

Diff for: js/angular/service/ionicConfig.js

+7
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,9 @@ IonicModule
222222
form: {
223223
checkbox: PLATFORM
224224
},
225+
scrolling: {
226+
jsScrolling: PLATFORM
227+
},
225228
tabs: {
226229
style: PLATFORM,
227230
position: PLATFORM
@@ -262,6 +265,10 @@ IonicModule
262265
checkbox: 'circle'
263266
},
264267

268+
scrolling: {
269+
jsScrolling: true
270+
},
271+
265272
tabs: {
266273
style: 'standard',
267274
position: 'bottom'

Diff for: scss/_scaffolding.scss

+16-4
Original file line numberDiff line numberDiff line change
@@ -261,25 +261,37 @@ body.grade-c {
261261
ion-infinite-scroll {
262262
height: 60px;
263263
width: 100%;
264-
opacity: 0;
264+
265265
display: block;
266266

267-
@include transition(opacity 0.25s);
267+
// @include transition(opacity 0.25s);
268268
@include display-flex();
269269
@include flex-direction(row);
270270
@include justify-content(center);
271271
@include align-items(center);
272272

273273
.icon {
274+
274275
color: #666666;
275276
font-size: 30px;
276277
color: $scroll-refresh-icon-color;
278+
&:before{
279+
-webkit-transform: translate3d(0,0,0);
280+
transform: translate3d(0,0,0);
281+
}
277282
}
283+
&:not(.active) .icon:before{
284+
-webkit-transform: translate3d(-1000px,0,0);
285+
transform: translate3d(-1000px,0,0);
278286

279-
&.active {
280-
opacity: 1;
281287
}
282288
}
289+
// removing the animation when the spinner isn't shown
290+
// this breaks up animations on iOS, so they are left with unnecessary reflows
291+
body:not(.platform-ios) ion-infinite-scroll:not(.active) .icon{
292+
-webkit-animation: none;
293+
animation:none;
294+
}
283295

284296
.overflow-scroll {
285297
overflow-x: hidden;

Diff for: test/unit/angular/directive/content.unit.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
describe('Ionic Content directive', function() {
2-
var compile, scope, timeout, window;
2+
var compile, scope, timeout, window, ionicConfig;
33

44
beforeEach(module('ionic'));
55

6-
beforeEach(inject(function($compile, $rootScope, $timeout, $window) {
6+
beforeEach(inject(function($compile, $rootScope, $timeout, $window, $ionicConfig) {
77
compile = $compile;
88
scope = $rootScope;
99
timeout = $timeout;
1010
window = $window;
11+
ionicConfig = $ionicConfig;
1112
ionic.Platform.setPlatform('Android');
1213
}));
1314

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

132+
it('Should allow native scrolling to be set by $ionicConfig ', function() {
133+
ionicConfig.scrolling.jsScrolling(false);
134+
var element = compile('<ion-content></ion-content>')(scope);
135+
expect(element.hasClass('overflow-scroll')).toBe(true);
136+
});
137+
131138
});
132139
/* Tests #555, #1155 */
133140
describe('Ionic Content Directive scoping', function() {

0 commit comments

Comments
 (0)