ionic-angular
Version:
[](https://circleci.com/gh/driftyco/ionic)
384 lines (318 loc) • 11.1 kB
JavaScript
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) && $attrs.spinner != 'none';
$scope.showIcon = isDefined($attrs.refreshingIcon);
$ionicBind($scope, $attrs, {
pullingIcon: '@',
pullingText: '@',
refreshingIcon: '@',
refreshingText: '@',
spinner: '@',
disablePullingRotation: '@',
$onRefresh: '&onRefresh',
$onPulling: '&onPulling'
});
function handleMousedown(e) {
e.touches = e.touches || [{
screenX: e.screenX,
screenY: e.screenY
}];
// Mouse needs this
startY = Math.floor(e.touches[0].screenY);
}
function handleTouchstart(e) {
e.touches = e.touches || [{
screenX: e.screenX,
screenY: e.screenY
}];
startY = e.touches[0].screenY;
}
function handleTouchend() {
// reset Y
startY = null;
// if this wasn't an overscroll, get out immediately
if (!canOverscroll && !isDragging) {
return;
}
// the user has overscrolled but went back to native scrolling
if (!isDragging) {
dragOffset = 0;
isOverscrolling = false;
setScrollLock(false);
} else {
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;
}
}
}
function handleTouchmove(e) {
e.touches = e.touches || [{
screenX: e.screenX,
screenY: e.screenY
}];
// Force mouse events to have had a down event first
if (!startY && e.type == 'mousemove') {
return;
}
// 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 = e.touches[0].screenY;
}
deltaY = e.touches[0].screenY - startY;
// how far have we dragged so far?
// kitkat fix for touchcancel events http://updates.html5rocks.com/2014/05/A-More-Compatible-Smoother-Touch
// Only do this if we're not on crosswalk
if (ionic.Platform.isAndroid() && ionic.Platform.version() === 4.4 && !ionic.Platform.isCrosswalk() && scrollParent.scrollTop === 0 && deltaY > 0) {
isDragging = true;
e.preventDefault();
}
// 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, deltaY - dragOffset * -1);
}
// if we're not at overscroll 0 yet, 0 out
if (lastOverscroll !== 0) {
overscroll(0);
}
return;
} 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((deltaY - dragOffset) / 3);
// 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] = 'translate3d(0px, ' + val + 'px, 0px)';
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(Math.floor((easedT * (Y - from)) + from));
if (time < 1) {
ionic.requestAnimationFrame(scroll);
} else {
if (Y < 5 && Y > -5) {
isOverscrolling = false;
setScrollLock(false);
}
callback && callback();
}
}
// start scroll loop
ionic.requestAnimationFrame(scroll);
}
var touchStartEvent, touchMoveEvent, touchEndEvent;
if (window.navigator.pointerEnabled) {
touchStartEvent = 'pointerdown';
touchMoveEvent = 'pointermove';
touchEndEvent = 'pointerup';
} else if (window.navigator.msPointerEnabled) {
touchStartEvent = 'MSPointerDown';
touchMoveEvent = 'MSPointerMove';
touchEndEvent = 'MSPointerUp';
} else {
touchStartEvent = 'touchstart';
touchMoveEvent = 'touchmove';
touchEndEvent = 'touchend';
}
self.init = function() {
scrollParent = $element.parent().parent()[0];
scrollChild = $element.parent()[0];
if (!scrollParent || !scrollParent.classList.contains('ionic-scroll') ||
!scrollChild || !scrollChild.classList.contains('scroll')) {
throw new Error('Refresher must be immediate child of ion-content or ion-scroll');
}
ionic.on(touchStartEvent, handleTouchstart, scrollChild);
ionic.on(touchMoveEvent, handleTouchmove, scrollChild);
ionic.on(touchEndEvent, handleTouchend, scrollChild);
ionic.on('mousedown', handleMousedown, scrollChild);
ionic.on('mousemove', handleTouchmove, scrollChild);
ionic.on('mouseup', handleTouchend, scrollChild);
ionic.on('scroll', handleScroll, scrollParent);
// cleanup when done
$scope.$on('$destroy', destroy);
};
function destroy() {
if ( scrollChild ) {
ionic.off(touchStartEvent, handleTouchstart, scrollChild);
ionic.off(touchMoveEvent, handleTouchmove, scrollChild);
ionic.off(touchEndEvent, handleTouchend, scrollChild);
ionic.off('mousedown', handleMousedown, scrollChild);
ionic.off('mousemove', handleTouchmove, scrollChild);
ionic.off('mouseup', handleTouchend, scrollChild);
}
if ( scrollParent ) {
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');
var q = $scope.$onRefresh();
if (q && q.then) {
q['finally'](function() {
$scope.$broadcast('scroll.refreshComplete');
});
}
}
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; };
}
]);