diff --git a/js/angular/controller/refresherController.js b/js/angular/controller/refresherController.js new file mode 100644 index 00000000000..8d7f099e478 --- /dev/null +++ b/js/angular/controller/refresherController.js @@ -0,0 +1,313 @@ +IonicModule +.controller('$ionicRefresher', [ + '$scope', + '$attrs', + '$element', + '$ionicBind', + '$timeout', + function($scope, $attrs, $element, $ionicBind, $timeout) { + var self = this, + isDragging = false, + isOverscrolling = false, + dragOffset = 0, + lastOverscroll = 0, + ptrThreshold = 60, + activated = false, + scrollTime = 500, + startY = null, + deltaY = null, + canOverscroll = true, + scrollParent, + scrollChild; + + if (!isDefined($attrs.pullingIcon)) { + $attrs.$set('pullingIcon', 'ion-android-arrow-down'); + } + + $scope.showSpinner = !isDefined($attrs.refreshingIcon); + + $ionicBind($scope, $attrs, { + pullingIcon: '@', + pullingText: '@', + refreshingIcon: '@', + refreshingText: '@', + spinner: '@', + disablePullingRotation: '@', + $onRefresh: '&onRefresh', + $onPulling: '&onPulling' + }); + + function handleTouchend() { + // if this wasn't an overscroll, get out immediately + if (!canOverscroll && !isDragging) { + return; + } + // reset Y + startY = null; + // the user has overscrolled but went back to native scrolling + if (!isDragging) { + dragOffset = 0; + isOverscrolling = false; + setScrollLock(false); + return true; + } + isDragging = false; + dragOffset = 0; + + // the user has scroll far enough to trigger a refresh + if (lastOverscroll > ptrThreshold) { + start(); + scrollTo(ptrThreshold, scrollTime); + + // the user has overscrolled but not far enough to trigger a refresh + } else { + scrollTo(0, scrollTime, deactivate); + isOverscrolling = false; + } + return true; + } + + function handleTouchmove(e) { + // if multitouch or regular scroll event, get out immediately + if (!canOverscroll || e.touches.length > 1) { + return; + } + //if this is a new drag, keep track of where we start + if (startY === null) { + startY = parseInt(e.touches[0].screenY, 10); + } + + // how far have we dragged so far? + deltaY = parseInt(e.touches[0].screenY, 10) - startY; + + // if we've dragged up and back down in to native scroll territory + if (deltaY - dragOffset <= 0 || scrollParent.scrollTop !== 0) { + + if (isOverscrolling) { + isOverscrolling = false; + setScrollLock(false); + } + + if (isDragging) { + nativescroll(scrollParent,parseInt(deltaY - dragOffset, 10) * -1); + } + + // if we're not at overscroll 0 yet, 0 out + if (lastOverscroll !== 0) { + overscroll(0); + } + + return true; + + } else if (deltaY > 0 && scrollParent.scrollTop === 0 && !isOverscrolling) { + // starting overscroll, but drag started below scrollTop 0, so we need to offset the position + dragOffset = deltaY; + } + + // prevent native scroll events while overscrolling + e.preventDefault(); + + // if not overscrolling yet, initiate overscrolling + if (!isOverscrolling) { + isOverscrolling = true; + setScrollLock(true); + } + + isDragging = true; + // overscroll according to the user's drag so far + overscroll(parseInt(deltaY - dragOffset, 10)); + + // update the icon accordingly + if (!activated && lastOverscroll > ptrThreshold) { + activated = true; + ionic.requestAnimationFrame(activate); + + } else if (activated && lastOverscroll < ptrThreshold) { + activated = false; + ionic.requestAnimationFrame(deactivate); + } + } + + function handleScroll(e) { + // canOverscrol is used to greatly simplify the drag handler during normal scrolling + canOverscroll = (e.target.scrollTop === 0) || isDragging; + } + + function overscroll(val) { + scrollChild.style[ionic.CSS.TRANSFORM] = 'translateY(' + val + 'px)'; + lastOverscroll = val; + } + + function nativescroll(target, newScrollTop) { + // creates a scroll event that bubbles, can be cancelled, and with its view + // and detail property initialized to window and 1, respectively + target.scrollTop = newScrollTop; + var e = document.createEvent("UIEvents"); + e.initUIEvent("scroll", true, true, window, 1); + target.dispatchEvent(e); + } + + function setScrollLock(enabled) { + // set the scrollbar to be position:fixed in preparation to overscroll + // or remove it so the app can be natively scrolled + if (enabled) { + ionic.requestAnimationFrame(function() { + scrollChild.classList.add('overscroll'); + show(); + }); + + } else { + ionic.requestAnimationFrame(function() { + scrollChild.classList.remove('overscroll'); + hide(); + deactivate(); + }); + } + } + + $scope.$on('scroll.refreshComplete', function() { + // prevent the complete from firing before the scroll has started + $timeout(function() { + + ionic.requestAnimationFrame(tail); + + // scroll back to home during tail animation + scrollTo(0, scrollTime, deactivate); + + // return to native scrolling after tail animation has time to finish + $timeout(function() { + + if (isOverscrolling) { + isOverscrolling = false; + setScrollLock(false); + } + + }, scrollTime); + + }, scrollTime); + }); + + function scrollTo(Y, duration, callback) { + // scroll animation loop w/ easing + // credit https://gist.github.com/dezinezync/5487119 + var start = Date.now(), + from = lastOverscroll; + + if (from === Y) { + callback(); + return; /* Prevent scrolling to the Y point if already there */ + } + + // decelerating to zero velocity + function easeOutCubic(t) { + return (--t) * t * t + 1; + } + + // scroll loop + function scroll() { + var currentTime = Date.now(), + time = Math.min(1, ((currentTime - start) / duration)), + // where .5 would be 50% of time on a linear scale easedT gives a + // fraction based on the easing method + easedT = easeOutCubic(time); + + overscroll(parseInt((easedT * (Y - from)) + from, 10)); + + if (time < 1) { + ionic.requestAnimationFrame(scroll); + + } else { + + if (Y < 5 && Y > -5) { + isOverscrolling = false; + setScrollLock(false); + } + + callback && callback(); + } + } + + // start scroll loop + ionic.requestAnimationFrame(scroll); + } + + + self.init = function() { + scrollParent = $element.parent().parent()[0]; + scrollChild = $element.parent()[0]; + + if (!scrollParent.classList.contains('ionic-scroll') || + !scrollChild.classList.contains('scroll')) { + throw new Error('Refresher must be immediate child of ion-content or ion-scroll'); + } + + ionic.on('touchmove', handleTouchmove, scrollChild); + ionic.on('touchend', handleTouchend, scrollChild); + ionic.on('scroll', handleScroll, scrollParent); + }; + + + $scope.$on('$destroy', destroy); + + function destroy() { + ionic.off('dragdown', handleTouchmove, scrollChild); + ionic.off('dragend', handleTouchend, scrollChild); + ionic.off('scroll', handleScroll, scrollParent); + scrollParent = null; + scrollChild = null; + } + + // DOM manipulation and broadcast methods shared by JS and Native Scrolling + // getter used by JS Scrolling + self.getRefresherDomMethods = function() { + return { + activate: activate, + deactivate: deactivate, + start: start, + show: show, + hide: hide, + tail: tail + }; + }; + + function activate() { + $element[0].classList.add('active'); + $scope.$onPulling(); + } + + function deactivate() { + // give tail 150ms to finish + $timeout(function() { + // deactivateCallback + $element.removeClass('active refreshing refreshing-tail'); + if (activated) activated = false; + }, 150); + } + + function start() { + // startCallback + $element[0].classList.add('refreshing'); + $scope.$onRefresh(); + } + + function show() { + // showCallback + $element[0].classList.remove('invisible'); + } + + function hide() { + // showCallback + $element[0].classList.add('invisible'); + } + + function tail() { + // tailCallback + $element[0].classList.add('refreshing-tail'); + } + + // for testing + self.__handleTouchmove = handleTouchmove; + self.__getScrollChild = function() { return scrollChild; }; + self.__getScrollParent= function() { return scrollParent; }; + } +]); diff --git a/js/angular/controller/scrollController.js b/js/angular/controller/scrollController.js index 05d365b90b4..2dca9e0bc42 100644 --- a/js/angular/controller/scrollController.js +++ b/js/angular/controller/scrollController.js @@ -12,7 +12,16 @@ IonicModule '$document', '$ionicScrollDelegate', '$ionicHistory', -function($scope, scrollViewOptions, $timeout, $window, $location, $document, $ionicScrollDelegate, $ionicHistory) { + '$controller', +function($scope, + scrollViewOptions, + $timeout, + $window, + $location, + $document, + $ionicScrollDelegate, + $ionicHistory, + $controller) { var self = this; // for testing @@ -171,38 +180,17 @@ function($scope, scrollViewOptions, $timeout, $window, $location, $document, $io /** * @private */ - self._setRefresher = function(refresherScope, refresherElement) { - var refresher = self.refresher = refresherElement; + self._setRefresher = function( + refresherScope, + refresherElement, + refresherMethods + ) { + self.refresher = refresherElement; var refresherHeight = self.refresher.clientHeight || 60; - scrollView.activatePullToRefresh(refresherHeight, function() { - // activateCallback - refresher.classList.add('active'); - refresherScope.$onPulling(); - onPullProgress(1); - }, function() { - // deactivateCallback - refresher.classList.remove('active'); - refresher.classList.remove('refreshing'); - refresher.classList.remove('refreshing-tail'); - }, function() { - // startCallback - refresher.classList.add('refreshing'); - refresherScope.$onRefresh(); - }, function() { - // showCallback - refresher.classList.remove('invisible'); - }, function() { - // hideCallback - refresher.classList.add('invisible'); - }, function() { - // tailCallback - refresher.classList.add('refreshing-tail'); - }, onPullProgress); - - function onPullProgress(progress) { - $scope.$broadcast('$ionicRefresher.pullProgress', progress); - refresherScope.$onPullProgress && refresherScope.$onPullProgress(progress); - } + scrollView.activatePullToRefresh( + refresherHeight, + refresherMethods + ); }; }]); diff --git a/js/angular/directive/refresher.js b/js/angular/directive/refresher.js index d8840f75a48..91e2c8a472d 100644 --- a/js/angular/directive/refresher.js +++ b/js/angular/directive/refresher.js @@ -48,10 +48,6 @@ * of the refresher. * @param {expression=} on-pulling Called when the user starts to pull down * on the refresher. - * @param {expression=} on-pull-progress Repeatedly called as the user is pulling down - * the refresher. The callback should have a `progress` argument which will be a number - * from `0` and `1`. For example, if the user has pulled the refresher halfway - * down, its progress would be `0.5`. * @param {string=} pulling-icon The icon to display while the user is pulling down. * Default: 'ion-android-arrow-down'. * @param {string=} spinner The {@link ionic.directive:ionSpinner} icon to display @@ -64,13 +60,14 @@ * */ IonicModule -.directive('ionRefresher', ['$ionicBind', '$parse', function($ionicBind, $parse) { +.directive('ionRefresher', [function() { return { restrict: 'E', replace: true, - require: '^$ionicScroll', + require: ['?^$ionicScroll', 'ionRefresher'], + controller: '$ionicRefresher', template: - '
' + + '