import {Component, ElementRef, EventEmitter, Host, Input, Output} from 'angular2/core'
import {NgIf, NgClass} from 'angular2/common';
import {Content} from '../content/content';
import {Icon} from '../icon/icon';
import {isDefined, defaults} from '../../util/util';
import {raf, ready, CSS} from '../../util/dom';
/**
* @name Refresher
* @description
* Allows you to add pull-to-refresh to an Content component.
* Place it as the first child of your Content or Scroll element.
*
* When refreshing is complete, call `refresher.complete()` from your controller.
*
* @usage
* ```html
*
*
*
*
*
* ```
*
* ```ts
* export class MyClass {
*
* doRefresh(refresher) {
* console.log('Doing Refresh', refresher)
*
* setTimeout(() => {
* refresher.complete();
* console.log("Complete");
* }, 5000);
* }
*
* doStart(refresher) {
* console.log('Doing Start', refresher);
* }
*
* doPulling(refresher) {
* console.log('Pulling', refresher);
* }
*
* }
* ```
* @demo /docs/v2/demos/refresher/
*
*/
@Component({
selector: 'ion-refresher',
host: {
'[class.active]': 'isActive',
'[class.refreshing]': 'isRefreshing',
'[class.refreshingTail]': 'isRefreshingTail'
},
template:
'
' +
'
' +
'' +
'
' +
'
' +
'
' +
'' +
'
' +
'
' +
'
',
directives: [NgIf, NgClass, Icon]
})
export class Refresher {
private _ele: HTMLElement;
private _touchMoveListener;
private _touchEndListener;
private _handleScrollListener;
/**
* @private
*/
isActive: boolean;
/**
* @private
*/
isDragging: boolean = false;
/**
* @private
*/
isOverscrolling: boolean = false;
/**
* @private
*/
dragOffset: number = 0;
/**
* @private
*/
lastOverscroll: number = 0;
/**
* @private
*/
ptrThreshold: number = 0;
/**
* @private
*/
activated: boolean = false;
/**
* @private
*/
scrollTime: number = 500;
/**
* @private
*/
canOverscroll: boolean = true;
/**
* @private
*/
startY;
/**
* @private
*/
deltaY;
/**
* @private
*/
scrollHost;
/**
* @private
*/
scrollChild;
/**
* @private
*/
showIcon: boolean;
/**
* @private
*/
showSpinner: boolean;
/**
* @private
*/
isRefreshing: boolean;
/**
* @private
*/
isRefreshingTail: boolean;
/**
* @input {string} the icon you want to display when you begin to pull down
*/
@Input() pullingIcon: string;
/**
* @input {string} the text you want to display when you begin to pull down
*/
@Input() pullingText: string;
/**
* @input {string} the icon you want to display when performing a refresh
*/
@Input() refreshingIcon: string;
/**
* @input {string} the text you want to display when performing a refresh
*/
@Input() refreshingText: string;
/**
* @private
*/
@Input() spinner: string;
/**
* @output {event} When you are pulling down
*/
@Output() pulling: EventEmitter = new EventEmitter();
/**
* @output {event} When you are refreshing
*/
@Output() refresh: EventEmitter = new EventEmitter();
/**
* @output {event} When you start pulling down
*/
@Output() start: EventEmitter = new EventEmitter();
constructor(
@Host() private _content: Content,
_element: ElementRef
) {
this._ele = _element.nativeElement;
this._ele.classList.add('content');
}
/**
* @private
*/
ngOnInit() {
let sp = this._content.getNativeElement();
let sc = this._content.scrollElement;
this.startY = null;
this.deltaY = null;
this.scrollHost = sp;
this.scrollChild = sc;
defaults(this, {
pullingIcon: 'md-arrow-down',
refreshingIcon: 'ionic'
})
this.showSpinner = !isDefined(this.refreshingIcon) && this.spinner != 'none';
this.showIcon = isDefined(this.refreshingIcon);
this._touchMoveListener = this._handleTouchMove.bind(this);
this._touchEndListener = this._handleTouchEnd.bind(this);
this._handleScrollListener = this._handleScroll.bind(this);
sc.addEventListener('touchmove', this._touchMoveListener);
sc.addEventListener('touchend', this._touchEndListener);
sc.addEventListener('scroll', this._handleScrollListener);
}
/**
* @private
*/
ngOnDestroy() {
let sc = this._content.scrollElement;
sc.removeEventListener('touchmove', this._touchMoveListener);
sc.removeEventListener('touchend', this._touchEndListener);
sc.removeEventListener('scroll', this._handleScrollListener);
}
/**
* @private
* @param {TODO} val TODO
*/
overscroll(val) {
this.scrollChild.style[CSS.transform] = 'translateY(' + val + 'px)';
this.lastOverscroll = val;
}
/**
* @private
* @param {TODO} target TODO
* @param {TODO} newScrollTop TODO
*/
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);
}
/**
* @private
* @param {TODO} enabled TODO
*/
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) {
raf(() => {
this.scrollChild.classList.add('overscroll');
this.show();
});
} else {
raf(() => {
this.scrollChild.classList.remove('overscroll');
this.hide();
this.deactivate();
});
}
}
/**
* @private
*/
activate() {
//this.ele.classList.add('active');
this.isActive = true;
this.start.emit(this);
}
/**
* @private
*/
deactivate() {
// give tail 150ms to finish
setTimeout(() => {
this.isActive = false;
this.isRefreshing = false;
this.isRefreshingTail = false;
// deactivateCallback
if (this.activated) this.activated = false;
}, 150);
}
/**
* @private
*/
startRefresh() {
// startCallback
this.isRefreshing = true;
this.refresh.emit(this);
}
/**
* @private
*/
show() {
// showCallback
this._ele.classList.remove('invisible');
}
/**
* @private
*/
hide() {
// showCallback
this._ele.classList.add('invisible');
}
/**
* @private
*/
tail() {
// tailCallback
this._ele.classList.add('refreshing-tail');
}
/**
* @private
*/
complete() {
setTimeout(() => {
raf(this.tail.bind(this));
// scroll back to home during tail animation
this.scrollTo(0, this.scrollTime, this.deactivate.bind(this));
// return to native scrolling after tail animation has time to finish
setTimeout(() => {
if (this.isOverscrolling) {
this.isOverscrolling = false;
this.setScrollLock(false);
}
}, this.scrollTime);
}, this.scrollTime);
}
/**
* @private
* @param {TODO} Y TODO
* @param {TODO} duration TODO
* @param {Function} callback TODO
*/
scrollTo(Y, duration, callback?) {
// scroll animation loop w/ easing
// credit https://gist.github.com/dezinezync/5487119
var start = Date.now(),
from = this.lastOverscroll;
if (from === Y) {
callback && 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);
this.overscroll( Math.round((easedT * (Y - from)) + from) );
if (time < 1) {
raf(scroll.bind(this));
} else {
if (Y < 5 && Y > -5) {
this.isOverscrolling = false;
this.setScrollLock(false);
}
callback && callback();
}
}
// start scroll loop
raf(scroll.bind(this));
}
/**
* @private
* TODO
* @param {Event} e TODO
*/
_handleTouchMove(e) {
//console.debug('TOUCHMOVE', e);
// if multitouch or regular scroll event, get out immediately
if (!this.canOverscroll || e.touches.length > 1) {
return;
}
//if this is a new drag, keep track of where we start
if (this.startY === null) {
this.startY = parseInt(e.touches[0].screenY, 10);
}
// how far have we dragged so far?
this.deltaY = parseInt(e.touches[0].screenY, 10) - this.startY;
// if we've dragged up and back down in to native scroll territory
if (this.deltaY - this.dragOffset <= 0 || this.scrollHost.scrollTop !== 0) {
if (this.isOverscrolling) {
this.isOverscrolling = false;
this.setScrollLock(false);
}
if (this.isDragging) {
this.nativescroll(this.scrollHost, Math.round(this.deltaY - this.dragOffset) * -1);
}
// if we're not at overscroll 0 yet, 0 out
if (this.lastOverscroll !== 0) {
this.overscroll(0);
}
return;
} else if (this.deltaY > 0 && this.scrollHost.scrollTop === 0 && !this.isOverscrolling) {
// starting overscroll, but drag started below scrollTop 0, so we need to offset the position
this.dragOffset = this.deltaY;
}
// prevent native scroll events while overscrolling
e.preventDefault();
// if not overscrolling yet, initiate overscrolling
if (!this.isOverscrolling) {
this.isOverscrolling = true;
this.setScrollLock(true);
}
this.isDragging = true;
// overscroll according to the user's drag so far
this.overscroll( Math.round((this.deltaY - this.dragOffset) / 3) );
// Pass the refresher to the EventEmitter
this.pulling.emit(this);
// update the icon accordingly
if (!this.activated && this.lastOverscroll > this.ptrThreshold) {
this.activated = true;
raf(this.activate.bind(this));
} else if (this.activated && this.lastOverscroll < this.ptrThreshold) {
this.activated = false;
raf(this.deactivate.bind(this));
}
}
/**
* @private
* TODO
* @param {Event} e TODO
*/
_handleTouchEnd(e) {
console.debug('TOUCHEND', e);
// if this wasn't an overscroll, get out immediately
if (!this.canOverscroll && !this.isDragging) {
return;
}
// reset Y
this.startY = null;
// the user has overscrolled but went back to native scrolling
if (!this.isDragging) {
this.dragOffset = 0;
this.isOverscrolling = false;
this.setScrollLock(false);
} else {
this.isDragging = false;
this.dragOffset = 0;
// the user has scroll far enough to trigger a refresh
if (this.lastOverscroll > this.ptrThreshold) {
this.startRefresh();
this.scrollTo(this.ptrThreshold, this.scrollTime);
// the user has overscrolled but not far enough to trigger a refresh
} else {
this.scrollTo(0, this.scrollTime, this.deactivate.bind(this));
this.isOverscrolling = false;
}
}
}
/**
* @private
* TODO
* @param {Event} e TODO
*/
_handleScroll(e) {
console.debug('SCROLL', e.target.scrollTop);
}
}