mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-21 04:53:58 +08:00
refactor(refresher): allow refresher content customization
Breaking Change: ## Refresher: - `<ion-refresher>` now takes a child `<ion-refresher-content>` component. - Custom refresh content components can now be replaced for Ionic's default refresher content. - Properties `pullingIcon`, `pullingText` and `refreshingText` have been moved to the `<ion-refresher-content>` component. - `spinner` property has been renamed to `refreshingSpinner` and now goes on the `<ion-refresher-content>` component. - `refreshingIcon` property is no longer an input, but instead `refreshingSpinner` should be used. Was: ``` <ion-refresher (refresh)="doRefresh($event)" pullingIcon="arrow-dropdown"> </ion-refresher> ``` Now: ``` <ion-refresher (refresh)="doRefresh($event)"> <ion-refresher-content pullingIcon="arrow-dropdown"></ion-refresher-content> </ion-refresher> ```
This commit is contained in:
@ -1,104 +0,0 @@
|
||||
// Scroll refresher (for pull to refresh)
|
||||
ion-refresher {
|
||||
position: absolute;
|
||||
top: -60px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
margin: auto;
|
||||
height: 60px;
|
||||
.refresher-content {
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
color: #000;//$scroll-refresh-icon-color;
|
||||
text-align: center;
|
||||
|
||||
font-size: 30px;
|
||||
|
||||
.text-refreshing,
|
||||
.text-pulling {
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
}
|
||||
&.refresher-with-text {
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-refreshing,
|
||||
.icon-pulling {
|
||||
width: 100%;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
-webkit-transform-style: preserve-3d;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
.icon-pulling {
|
||||
animation-name: refresh-spin-back;
|
||||
animation-duration: 200ms;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: none;
|
||||
transform: translate3d(0,0,0) rotate(0deg);
|
||||
}
|
||||
.icon-refreshing,
|
||||
.text-refreshing {
|
||||
display: none;
|
||||
}
|
||||
.icon-refreshing {
|
||||
animation-duration: 1.5s;
|
||||
}
|
||||
|
||||
&.active {
|
||||
.icon-pulling:not(.pulling-rotation-disabled) {
|
||||
animation-name: refresh-spin;
|
||||
transform: translate3d(0,0,0) rotate(-180deg);
|
||||
}
|
||||
&.refreshing {
|
||||
transition: -webkit-transform .2s;
|
||||
transition: transform .2s;
|
||||
transform: scale(1,1);
|
||||
|
||||
.icon-pulling,
|
||||
.text-pulling {
|
||||
display: none;
|
||||
}
|
||||
.icon-refreshing,
|
||||
.text-refreshing {
|
||||
display: block;
|
||||
}
|
||||
&.refreshing-tail {
|
||||
transform: scale(0,0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
scroll-content.overscroll {
|
||||
overflow: visible;
|
||||
}
|
||||
/*
|
||||
-webkit-overflow-scrolling:touch;
|
||||
width:100%;
|
||||
}
|
||||
*/
|
||||
|
||||
@-webkit-keyframes refresh-spin {
|
||||
0% { -webkit-transform: translate3d(0,0,0) rotate(0); }
|
||||
100% { -webkit-transform: translate3d(0,0,0) rotate(180deg); }
|
||||
}
|
||||
|
||||
@keyframes refresh-spin {
|
||||
0% { transform: translate3d(0,0,0) rotate(0); }
|
||||
100% { transform: translate3d(0,0,0) rotate(180deg); }
|
||||
}
|
||||
|
||||
@-webkit-keyframes refresh-spin-back {
|
||||
0% { -webkit-transform: translate3d(0,0,0) rotate(180deg); }
|
||||
100% { -webkit-transform: translate3d(0,0,0) rotate(0); }
|
||||
}
|
||||
|
||||
@keyframes refresh-spin-back {
|
||||
0% { transform: translate3d(0,0,0) rotate(180deg); }
|
||||
100% { transform: translate3d(0,0,0) rotate(0); }
|
||||
}
|
@ -1,544 +0,0 @@
|
||||
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
|
||||
* <ion-content>
|
||||
* <ion-refresher (start)="doStart($event)"
|
||||
* (refresh)="doRefresh($event)"
|
||||
* (pulling)="doPulling($event)">
|
||||
* </ion-refresher>
|
||||
*
|
||||
* </ion-content>
|
||||
|
||||
* ```
|
||||
*
|
||||
* ```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:
|
||||
'<div class="refresher-content" [class.refresher-with-text]="pullingText || refreshingText">' +
|
||||
'<div class="icon-pulling">' +
|
||||
'<ion-icon [name]="pullingIcon"></ion-icon>' +
|
||||
'</div>' +
|
||||
'<div class="text-pulling" [innerHTML]="pullingText" *ngIf="pullingText"></div>' +
|
||||
'<div class="icon-refreshing">' +
|
||||
'<ion-icon [name]="refreshingIcon"></ion-icon>' +
|
||||
'</div>' +
|
||||
'<div class="text-refreshing" [innerHTML]="refreshingText" *ngIf="refreshingText"></div>' +
|
||||
'</div>',
|
||||
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<Refresher> = new EventEmitter();
|
||||
|
||||
/**
|
||||
* @output {event} When you are refreshing
|
||||
*/
|
||||
@Output() refresh: EventEmitter<Refresher> = new EventEmitter();
|
||||
|
||||
/**
|
||||
* @output {event} When you start pulling down
|
||||
*/
|
||||
@Output() start: EventEmitter<Refresher> = 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);
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
import {App} from 'ionic-angular';
|
||||
|
||||
|
||||
@App({
|
||||
templateUrl: 'main.html'
|
||||
})
|
||||
class E2EApp {
|
||||
items = [];
|
||||
|
||||
constructor() {
|
||||
for(let i = 0; i < 20; i++) {
|
||||
this.items.push({ "index": i });
|
||||
}
|
||||
}
|
||||
|
||||
doRefresh(refresher) {
|
||||
console.log('Doing Refresh', refresher)
|
||||
|
||||
// Add to the top of the list on refresh
|
||||
let firstIndex = this.items[0].index - 1;
|
||||
|
||||
for(let i = firstIndex; i > firstIndex - 5; i--) {
|
||||
this.items.unshift({ "index": i });
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
refresher.complete();
|
||||
console.log("Complete");
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
doStart(refresher) {
|
||||
console.log('Doing Start', refresher);
|
||||
}
|
||||
|
||||
doPulling(refresher) {
|
||||
console.log('Pulling', refresher);
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
<ion-toolbar><ion-title>Pull To Refresh</ion-title></ion-toolbar>
|
||||
|
||||
<ion-content>
|
||||
<ion-refresher
|
||||
(start)="doStart($event)"
|
||||
(refresh)="doRefresh($event)"
|
||||
(pulling)="doPulling($event)"
|
||||
pullingIcon="heart"
|
||||
pullingText="release to refresh..."
|
||||
refreshingIcon="star"
|
||||
refreshingText="refreshing...">
|
||||
</ion-refresher>
|
||||
<ion-list>
|
||||
<ion-item *ngFor="#item of items">
|
||||
Item {{ item.index }}
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
Reference in New Issue
Block a user