mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-20 20:33:32 +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,22 +1,31 @@
|
|||||||
import {App, Page, IonicApp} from 'ionic-angular';
|
import {App, Page, Refresher} from 'ionic-angular';
|
||||||
|
import {MockProvider} from './mock-provider';
|
||||||
|
|
||||||
|
|
||||||
@App({
|
@App({
|
||||||
templateUrl: 'main.html'
|
templateUrl: 'main.html',
|
||||||
|
providers: [MockProvider]
|
||||||
})
|
})
|
||||||
class ApiDemoApp {
|
class ApiDemoApp {
|
||||||
doRefresh(refresher) {
|
items: string[];
|
||||||
console.log('DOREFRESH', refresher)
|
|
||||||
|
|
||||||
setTimeout(() => {
|
constructor(private mockProvider: MockProvider) {
|
||||||
refresher.complete();
|
this.items = mockProvider.getData();
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
doStarting() {
|
doRefresh(refresher: Refresher) {
|
||||||
console.log('DOSTARTING');
|
console.log('DOREFRESH', refresher);
|
||||||
|
|
||||||
|
this.mockProvider.getAsyncData().then((newData) => {
|
||||||
|
for (var i = 0; i < newData.length; i++) {
|
||||||
|
this.items.unshift( newData[i] );
|
||||||
|
}
|
||||||
|
|
||||||
|
refresher.endRefreshing();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
doPulling(amt) {
|
doPulling(refresher: Refresher) {
|
||||||
console.log('DOPULLING', amt);
|
console.log('DOPULLING', refresher.progress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,20 @@
|
|||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ion-title>Refresher</ion-title>
|
<ion-title>Pull To Refresh</ion-title>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
|
|
||||||
|
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<ion-refresher (starting)="doStarting()" (refresh)="doRefresh($event, refresher)" (pulling)="doPulling($event, amt)">
|
<ion-refresher (refresh)="doRefresh($event)" (pulling)="doPulling($event)">
|
||||||
|
<ion-refresher-content
|
||||||
|
pullingText="Pull to refresh..."
|
||||||
|
refreshingText="Refreshing...">
|
||||||
|
</ion-refresher-content>
|
||||||
</ion-refresher>
|
</ion-refresher>
|
||||||
|
|
||||||
<ion-list>
|
<ion-list>
|
||||||
<ion-item>Item 1</ion-item>
|
<ion-item *ngFor="#item of items">
|
||||||
<ion-item>Item 2</ion-item>
|
{{ item }}
|
||||||
<ion-item>Item 3</ion-item>
|
</ion-item>
|
||||||
<ion-item>Item 4</ion-item>
|
|
||||||
<ion-item>Item 5</ion-item>
|
|
||||||
<ion-item>Item 6</ion-item>
|
|
||||||
<ion-item>Item 7</ion-item>
|
|
||||||
<ion-item>Item 8</ion-item>
|
|
||||||
<ion-item>Item 9</ion-item>
|
|
||||||
<ion-item>Item 10</ion-item>
|
|
||||||
<ion-item>Item 11</ion-item>
|
|
||||||
<ion-item>Item 12</ion-item>
|
|
||||||
<ion-item>Item 13</ion-item>
|
|
||||||
<ion-item>Item 14</ion-item>
|
|
||||||
<ion-item>Item 15</ion-item>
|
|
||||||
<ion-item>Item 16</ion-item>
|
|
||||||
<ion-item>Item 17</ion-item>
|
|
||||||
<ion-item>Item 18</ion-item>
|
|
||||||
<ion-item>Item 19</ion-item>
|
|
||||||
<ion-item>Item 20</ion-item>
|
|
||||||
</ion-list>
|
</ion-list>
|
||||||
|
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
61
demos/refresher/mock-provider.ts
Normal file
61
demos/refresher/mock-provider.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import {Injectable} from 'angular2/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock Data Access Object
|
||||||
|
**/
|
||||||
|
@Injectable()
|
||||||
|
export class MockProvider {
|
||||||
|
|
||||||
|
getData() {
|
||||||
|
// return mock data synchronously
|
||||||
|
let data = [];
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
data.push( this._getRandomData() );
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAsyncData() {
|
||||||
|
// async receive mock data
|
||||||
|
return new Promise(resolve => {
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(this.getData());
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getRandomData() {
|
||||||
|
let i = Math.floor( Math.random() * this._data.length );
|
||||||
|
return this._data[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _data = [
|
||||||
|
'Fast Times at Ridgemont High',
|
||||||
|
'Peggy Sue Got Married',
|
||||||
|
'Raising Arizona',
|
||||||
|
'Moonstruck',
|
||||||
|
'Fire Birds',
|
||||||
|
'Honeymoon in Vegas',
|
||||||
|
'Amos & Andrew',
|
||||||
|
'It Could Happen to You',
|
||||||
|
'Trapped in Paradise',
|
||||||
|
'Leaving Las Vegas',
|
||||||
|
'The Rock',
|
||||||
|
'Con Air',
|
||||||
|
'Face/Off',
|
||||||
|
'City of Angels',
|
||||||
|
'Gone in Sixty Seconds',
|
||||||
|
'The Family Man',
|
||||||
|
'Windtalkers',
|
||||||
|
'Matchstick Men',
|
||||||
|
'National Treasure',
|
||||||
|
'Ghost Rider',
|
||||||
|
'Grindhouse',
|
||||||
|
'Next',
|
||||||
|
'Kick-Ass',
|
||||||
|
'Drive Angry',
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
@ -18,8 +18,8 @@
|
|||||||
"components/icon/icon",
|
"components/icon/icon",
|
||||||
"components/menu/menu",
|
"components/menu/menu",
|
||||||
"components/modal/modal",
|
"components/modal/modal",
|
||||||
|
"components/refresher/refresher",
|
||||||
"components/scroll/scroll",
|
"components/scroll/scroll",
|
||||||
"components/scroll/pull-to-refresh",
|
|
||||||
"components/slides/slides",
|
"components/slides/slides",
|
||||||
"components/spinner/spinner";
|
"components/spinner/spinner";
|
||||||
|
|
||||||
|
@ -31,8 +31,9 @@ export * from './components/overlay/overlay'
|
|||||||
export * from './components/slides/slides'
|
export * from './components/slides/slides'
|
||||||
export * from './components/radio/radio-button'
|
export * from './components/radio/radio-button'
|
||||||
export * from './components/radio/radio-group'
|
export * from './components/radio/radio-group'
|
||||||
|
export * from './components/refresher/refresher'
|
||||||
|
export * from './components/refresher/refresher-content'
|
||||||
export * from './components/scroll/scroll'
|
export * from './components/scroll/scroll'
|
||||||
export * from './components/scroll/pull-to-refresh'
|
|
||||||
export * from './components/searchbar/searchbar'
|
export * from './components/searchbar/searchbar'
|
||||||
export * from './components/segment/segment'
|
export * from './components/segment/segment'
|
||||||
export * from './components/select/select'
|
export * from './components/select/select'
|
||||||
|
@ -9,6 +9,9 @@ $z-index-menu-backdrop: 79;
|
|||||||
$z-index-overlay: 1000;
|
$z-index-overlay: 1000;
|
||||||
$z-index-click-block: 9999;
|
$z-index-click-block: 9999;
|
||||||
|
|
||||||
|
$z-index-scroll-content: 1;
|
||||||
|
$z-index-refresher: 0;
|
||||||
|
|
||||||
$z-index-navbar-section: 10;
|
$z-index-navbar-section: 10;
|
||||||
|
|
||||||
$z-index-toolbar: 10;
|
$z-index-toolbar: 10;
|
||||||
@ -129,6 +132,7 @@ ion-content {
|
|||||||
|
|
||||||
scroll-content {
|
scroll-content {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
z-index: $z-index-scroll-content;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
@ -3,7 +3,7 @@ import {Component, ElementRef, Optional, NgZone} from 'angular2/core';
|
|||||||
import {Ion} from '../ion';
|
import {Ion} from '../ion';
|
||||||
import {IonicApp} from '../app/app';
|
import {IonicApp} from '../app/app';
|
||||||
import {Config} from '../../config/config';
|
import {Config} from '../../config/config';
|
||||||
import {raf} from '../../util/dom';
|
import {raf, transitionEnd} from '../../util/dom';
|
||||||
import {ViewController} from '../nav/view-controller';
|
import {ViewController} from '../nav/view-controller';
|
||||||
import {Animation} from '../../animations/animation';
|
import {Animation} from '../../animations/animation';
|
||||||
import {ScrollTo} from '../../animations/scroll-to';
|
import {ScrollTo} from '../../animations/scroll-to';
|
||||||
@ -11,15 +11,15 @@ import {ScrollTo} from '../../animations/scroll-to';
|
|||||||
/**
|
/**
|
||||||
* @name Content
|
* @name Content
|
||||||
* @description
|
* @description
|
||||||
* The Content component provides an easy to use content area that can be configured to use Ionic's custom Scroll View, or the built in overflow scrolling of the browser.
|
* The Content component provides an easy to use content area with some useful
|
||||||
|
* methods to control the scrollable area.
|
||||||
*
|
*
|
||||||
* While we recommend using the custom Scroll features in Ionic in most cases, sometimes (for performance reasons) only the browser's native overflow scrolling will suffice, and so we've made it easy to toggle between the Ionic scroll implementation and overflow scrolling.
|
* The content area can also implement pull-to-refresh with the
|
||||||
*
|
* [Refresher](../../scroll/Refresher) component.
|
||||||
* You can implement pull-to-refresh with the [Refresher](../../scroll/Refresher) component.
|
|
||||||
*
|
*
|
||||||
* @usage
|
* @usage
|
||||||
* ```html
|
* ```html
|
||||||
* <ion-content id="myContent">
|
* <ion-content>
|
||||||
* Add your content here!
|
* Add your content here!
|
||||||
* </ion-content>
|
* </ion-content>
|
||||||
* ```
|
* ```
|
||||||
@ -30,7 +30,8 @@ import {ScrollTo} from '../../animations/scroll-to';
|
|||||||
template:
|
template:
|
||||||
'<scroll-content>' +
|
'<scroll-content>' +
|
||||||
'<ng-content></ng-content>' +
|
'<ng-content></ng-content>' +
|
||||||
'</scroll-content>'
|
'</scroll-content>' +
|
||||||
|
'<ng-content select="ion-refresher"></ng-content>'
|
||||||
})
|
})
|
||||||
export class Content extends Ion {
|
export class Content extends Ion {
|
||||||
private _padding: number = 0;
|
private _padding: number = 0;
|
||||||
@ -42,10 +43,6 @@ export class Content extends Ion {
|
|||||||
*/
|
*/
|
||||||
scrollElement: HTMLElement;
|
scrollElement: HTMLElement;
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {elementRef} elementRef A reference to the component's DOM element.
|
|
||||||
* @param {config} config The config object to change content's default settings.
|
|
||||||
*/
|
|
||||||
constructor(
|
constructor(
|
||||||
private _elementRef: ElementRef,
|
private _elementRef: ElementRef,
|
||||||
private _config: Config,
|
private _config: Config,
|
||||||
@ -83,7 +80,8 @@ export class Content extends Ion {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.scrollElement.removeEventListener('scroll', this._onScroll);
|
this.scrollElement.removeEventListener('scroll', this._onScroll.bind(this));
|
||||||
|
this.scrollElement = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -112,28 +110,70 @@ export class Content extends Ion {
|
|||||||
* @param {Function} handler The method you want perform when scrolling
|
* @param {Function} handler The method you want perform when scrolling
|
||||||
* @returns {Function} A function that removes the scroll handler.
|
* @returns {Function} A function that removes the scroll handler.
|
||||||
*/
|
*/
|
||||||
addScrollEventListener(handler) {
|
addScrollListener(handler) {
|
||||||
if (!this.scrollElement) {
|
return this._addListener('scroll', handler);
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
addTouchStartListener(handler) {
|
||||||
|
return this._addListener('touchstart', handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
addTouchMoveListener(handler) {
|
||||||
|
return this._addListener('touchmove', handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
addTouchEndListener(handler) {
|
||||||
|
return this._addListener('touchend', handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
addMouseDownListener(handler) {
|
||||||
|
return this._addListener('mousedown', handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
addMouseUpListener(handler) {
|
||||||
|
return this._addListener('mouseup', handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
addMouseMoveListener(handler) {
|
||||||
|
return this._addListener('mousemove', handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addListener(type: string, handler: any): Function {
|
||||||
|
if (!this.scrollElement) { return; }
|
||||||
|
|
||||||
// ensure we're not creating duplicates
|
// ensure we're not creating duplicates
|
||||||
this.scrollElement.removeEventListener('scroll', handler);
|
this.scrollElement.removeEventListener(type, handler);
|
||||||
|
this.scrollElement.addEventListener(type, handler);
|
||||||
this.scrollElement.addEventListener('scroll', handler);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
this.scrollElement.removeEventListener('scroll', handler);
|
this.scrollElement.removeEventListener(type, handler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @private
|
||||||
* Call a method when scrolling has stopped
|
* Call a method when scrolling has stopped
|
||||||
*
|
|
||||||
* @param {Function} callback The method you want perform when scrolling has ended
|
* @param {Function} callback The method you want perform when scrolling has ended
|
||||||
*/
|
*/
|
||||||
onScrollEnd(callback) {
|
onScrollEnd(callback: Function) {
|
||||||
let lastScrollTop = null;
|
let lastScrollTop = null;
|
||||||
let framesUnchanged = 0;
|
let framesUnchanged = 0;
|
||||||
let _scrollEle = this.scrollElement;
|
let _scrollEle = this.scrollElement;
|
||||||
@ -163,43 +203,8 @@ export class Content extends Ion {
|
|||||||
setTimeout(next, 100);
|
setTimeout(next, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
onScrollElementTransitionEnd(callback: Function) {
|
||||||
* @private
|
transitionEnd(this.scrollElement, callback);
|
||||||
* Adds the specified touchmove handler to the content's scroll element.
|
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* @Page({
|
|
||||||
* template: `<ion-content id="my-content"></ion-content>`
|
|
||||||
* )}
|
|
||||||
* export class MyPage{
|
|
||||||
* constructor(app: IonicApp){
|
|
||||||
* this.app = app;
|
|
||||||
* }
|
|
||||||
* // Need to wait until the component has been initialized
|
|
||||||
* ngAfterViewInit() {
|
|
||||||
* // Here 'my-content' is the ID of my ion-content
|
|
||||||
* this.content = this.app.getComponent('my-content');
|
|
||||||
* this.content.addTouchMoveListener(this.touchHandler);
|
|
||||||
* }
|
|
||||||
* touchHandler() {
|
|
||||||
* console.log("I'm touching all the magazines!!");
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
* @param {Function} handler The method you want to perform when touchmove is firing
|
|
||||||
* @returns {Function} A function that removes the touchmove handler.
|
|
||||||
*/
|
|
||||||
addTouchMoveListener(handler) {
|
|
||||||
if (!this.scrollElement) { return; }
|
|
||||||
|
|
||||||
// ensure we're not creating duplicates
|
|
||||||
this.scrollElement.removeEventListener('touchmove', handler);
|
|
||||||
|
|
||||||
this.scrollElement.addEventListener('touchmove', handler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
this.scrollElement.removeEventListener('touchmove', handler);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -276,6 +281,22 @@ export class Content extends Ion {
|
|||||||
return this._scrollTo.start(0, 0, 300, 0);
|
return this._scrollTo.start(0, 0, 300, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getScrollTop(): number {
|
||||||
|
return this.getNativeElement().scrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
addCssClass(className: string) {
|
||||||
|
this.getNativeElement().classList.add(className);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeCssClass(className: string) {
|
||||||
|
this.getNativeElement().classList.remove(className);
|
||||||
|
}
|
||||||
|
|
||||||
|
setScrollElementStyle(prop: string, val: any) {
|
||||||
|
this.scrollElement.style[prop] = val;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* Returns the content and scroll elements' dimensions.
|
* Returns the content and scroll elements' dimensions.
|
||||||
|
@ -438,7 +438,7 @@ export class InputBase {
|
|||||||
if (this._useAssist && this._scrollView) {
|
if (this._useAssist && this._scrollView) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.deregScrollMove();
|
this.deregScrollMove();
|
||||||
this._deregScroll = this._scrollView.addScrollEventListener(this._scrollMove);
|
this._deregScroll = this._scrollView.addScrollListener(this._scrollMove);
|
||||||
}, 80);
|
}, 80);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import {ElementRef} from 'angular2/core';
|
import {ElementRef} from 'angular2/core';
|
||||||
import {Config} from '../config/config';
|
|
||||||
import {isArray} from '../util';
|
|
||||||
import * as dom from '../util/dom';
|
import * as dom from '../util/dom';
|
||||||
|
|
||||||
let ids:number = 0;
|
let ids:number = 0;
|
||||||
@ -17,24 +15,30 @@ export class Ion {
|
|||||||
this._id = 'i' + ids++;
|
this._id = 'i' + ids++;
|
||||||
}
|
}
|
||||||
|
|
||||||
getElementRef() {
|
getElementRef(): ElementRef {
|
||||||
return this.elementRef;
|
return this.elementRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
getNativeElement() {
|
getNativeElement(): any {
|
||||||
return this.elementRef.nativeElement;
|
return this.elementRef.nativeElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDimensions() {
|
getDimensions(): {
|
||||||
|
width: number, height: number, left: number, top: number
|
||||||
|
} {
|
||||||
return dom.getDimensions(this.elementRef.nativeElement, this._id);
|
return dom.getDimensions(this.elementRef.nativeElement, this._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
width() {
|
width(): number {
|
||||||
return dom.getDimensions(this.elementRef.nativeElement, this._id).width;
|
return dom.getDimensions(this.elementRef.nativeElement, this._id).width;
|
||||||
}
|
}
|
||||||
|
|
||||||
height() {
|
height(): number {
|
||||||
return dom.getDimensions(this.elementRef.nativeElement, this._id).height;
|
return dom.getDimensions(this.elementRef.nativeElement, this._id).height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
dom.clearDimensions(this._id);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
69
ionic/components/refresher/refresher-content.ts
Normal file
69
ionic/components/refresher/refresher-content.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import {Component, Input} from 'angular2/core'
|
||||||
|
import {NgIf} from 'angular2/common';
|
||||||
|
|
||||||
|
import {Config} from '../../config/config';
|
||||||
|
import {Icon} from '../icon/icon';
|
||||||
|
import {Refresher} from './refresher';
|
||||||
|
import {Spinner} from '../spinner/spinner';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ion-refresher-content',
|
||||||
|
template:
|
||||||
|
'<div class="refresher-pulling">' +
|
||||||
|
'<div class="refresher-pulling-icon" *ngIf="pullingIcon">' +
|
||||||
|
'<ion-icon [name]="pullingIcon"></ion-icon>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="refresher-pulling-text" [innerHTML]="pullingText" *ngIf="pullingText"></div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="refresher-refreshing">' +
|
||||||
|
'<div class="refresher-refreshing-icon">' +
|
||||||
|
'<ion-spinner [name]="refreshingSpinner"></ion-spinner>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="refresher-refreshing-text" [innerHTML]="refreshingText" *ngIf="refreshingText"></div>' +
|
||||||
|
'</div>',
|
||||||
|
directives: [NgIf, Icon, Spinner],
|
||||||
|
host: {
|
||||||
|
'[attr.state]': 'r.state'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export class RefresherContent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @input {string} a static icon 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} An animated SVG spinner that shows when refreshing begins
|
||||||
|
*/
|
||||||
|
@Input() refreshingSpinner: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @input {string} the text you want to display when performing a refresh
|
||||||
|
*/
|
||||||
|
@Input() refreshingText: string;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(private r: Refresher, private _config: Config) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ngOnInit() {
|
||||||
|
if (!this.pullingIcon) {
|
||||||
|
this.pullingIcon = this._config.get('pullingIcon', 'arrow-down');
|
||||||
|
}
|
||||||
|
if (!this.refreshingSpinner) {
|
||||||
|
this.refreshingSpinner = this._config.get('refreshingSpinner', 'ios');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
112
ionic/components/refresher/refresher.scss
Normal file
112
ionic/components/refresher/refresher.scss
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
@import "../../globals.core";
|
||||||
|
|
||||||
|
// Refresher
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
$refresher-height: 60px !default;
|
||||||
|
|
||||||
|
$refresher-icon-color: #000 !default;
|
||||||
|
$refresher-icon-font-size: 30px !default;
|
||||||
|
|
||||||
|
$refresher-text-color: #000 !default;
|
||||||
|
$refresher-text-font-size: 16px !default;
|
||||||
|
|
||||||
|
|
||||||
|
ion-refresher {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: $z-index-refresher;
|
||||||
|
width: 100%;
|
||||||
|
height: $refresher-height;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
&.refresher-active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-refresher > scroll-content {
|
||||||
|
// when the refresher is let go or has completed
|
||||||
|
// this transition is what is used to put the
|
||||||
|
// scroll content back into it's original location
|
||||||
|
transition: all 320ms cubic-bezier(0.36,0.66,0.04,1);
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Refresher Content
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
ion-refresher-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresher-pulling,
|
||||||
|
.refresher-refreshing {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresher-pulling-icon,
|
||||||
|
.refresher-refreshing-icon {
|
||||||
|
text-align: center;
|
||||||
|
color: $refresher-icon-color;
|
||||||
|
font-size: $refresher-icon-font-size;
|
||||||
|
transition: 200ms;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresher-pulling-text,
|
||||||
|
.refresher-refreshing-text {
|
||||||
|
text-align: center;
|
||||||
|
color: $refresher-text-color;
|
||||||
|
font-size: $refresher-text-font-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Refresher Content States
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
ion-refresher-content[state=pulling] {
|
||||||
|
.refresher-pulling {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-refresher-content[state=ready] {
|
||||||
|
.refresher-pulling {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.refresher-pulling-icon {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-refresher-content[state=refreshing] {
|
||||||
|
.refresher-refreshing {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-refresher-content[state=cancelling] {
|
||||||
|
.refresher-pulling {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.refresher-pulling-icon {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-refresher-content[state=ending] {
|
||||||
|
.refresher-refreshing {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.refresher-refreshing-icon {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
}
|
549
ionic/components/refresher/refresher.ts
Normal file
549
ionic/components/refresher/refresher.ts
Normal file
@ -0,0 +1,549 @@
|
|||||||
|
import {Directive, ElementRef, EventEmitter, Host, Input, Output, NgZone} from 'angular2/core'
|
||||||
|
import {NgIf, NgClass} from 'angular2/common';
|
||||||
|
|
||||||
|
import {Content} from '../content/content';
|
||||||
|
import {Icon} from '../icon/icon';
|
||||||
|
import {isTrueProperty} from '../../util/util';
|
||||||
|
import {CSS, pointerCoord, transitionEnd} from '../../util/dom';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Refresher
|
||||||
|
* @description
|
||||||
|
* Allows you to add Pull-To-Refresh to an Content component.
|
||||||
|
* Place `ion-refresher` as the first child of your `ion-content` element.
|
||||||
|
*
|
||||||
|
* Pages can then can listen to the refreshers various output events. The
|
||||||
|
* `refresh` output event is the one that's fired when the user has pulled
|
||||||
|
* down far enough to kick off the refreshing process. Once the async operation
|
||||||
|
* has completed and the refreshing should end, call `endRefreshing()`.
|
||||||
|
*
|
||||||
|
* @usage
|
||||||
|
* ```html
|
||||||
|
* <ion-content>
|
||||||
|
*
|
||||||
|
* <ion-refresher (refresh)="doRefresh($event)">
|
||||||
|
* <ion-refresher-content></ion-refresher-content>
|
||||||
|
* </ion-refresher>
|
||||||
|
*
|
||||||
|
* </ion-content>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* @Page({...})
|
||||||
|
* export class NewsFeedPage {
|
||||||
|
*
|
||||||
|
* doRefresh(refresher) {
|
||||||
|
* console.log('Begin async operation', refresher);
|
||||||
|
*
|
||||||
|
* setTimeout(() => {
|
||||||
|
* console.log('Async operation has ended');
|
||||||
|
* refresher.endRefreshing();
|
||||||
|
* }, 2000);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* ## Refresher Content
|
||||||
|
*
|
||||||
|
* By default, Ionic provides the pulling icon and refreshing spinner that
|
||||||
|
* looks best for the platform the user is on. However, you can change the
|
||||||
|
* default icon and spinner, along with adding text for each state by
|
||||||
|
* adding properties to the child `ion-refresher-content` component.
|
||||||
|
*
|
||||||
|
* ```html
|
||||||
|
* <ion-content>
|
||||||
|
*
|
||||||
|
* <ion-refresher (refresh)="doRefresh($event)">
|
||||||
|
* <ion-refresher-content
|
||||||
|
* pullingIcon="arrow-dropdown"
|
||||||
|
* pullingText="Pull to refresh"
|
||||||
|
* refreshingSpinner="circles"
|
||||||
|
* refreshingText="Refreshing...">
|
||||||
|
* </ion-refresher-content>
|
||||||
|
* </ion-refresher>
|
||||||
|
*
|
||||||
|
* </ion-content>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* ## Further Customizing Refresher Content
|
||||||
|
*
|
||||||
|
* The `ion-refresh` component holds the refresh logic, and it requires a
|
||||||
|
* child refresher content component for its display. The `ion-refresher-content`
|
||||||
|
* component is Ionic's default that shows the actual display of the refresher
|
||||||
|
* and changes its look depending on the refresher's state. With this separation,
|
||||||
|
* it also allows developers to create their own refresher content components.
|
||||||
|
* Ideas include having some cool SVG or CSS animations that are customized to
|
||||||
|
* your app and animates the various refresher states to your liking.
|
||||||
|
*
|
||||||
|
* @demo /docs/v2/demos/refresher/
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Directive({
|
||||||
|
selector: 'ion-refresher',
|
||||||
|
host: {
|
||||||
|
'[class.refresher-active]': 'state !== "inactive"'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export class Refresher {
|
||||||
|
private _appliedStyles: boolean = false;
|
||||||
|
private _didStart: boolean;
|
||||||
|
private _lastStart: number = 0;
|
||||||
|
private _lastCheck: number = 0;
|
||||||
|
private _isEnabled: boolean = true;
|
||||||
|
private _mDown: Function;
|
||||||
|
private _mMove: Function;
|
||||||
|
private _mUp: Function;
|
||||||
|
private _tStart: Function;
|
||||||
|
private _tMove: Function;
|
||||||
|
private _tEnd: Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current state which the refresher is in. The refresher's states include:
|
||||||
|
*
|
||||||
|
* - `inactive` - The refresher is not being pulled down or refreshing and is currently hidden.
|
||||||
|
* - `pulling` - The user is actively pulling down the refresher, but has not reached the point yet that if the user lets go, it'll refresh.
|
||||||
|
* - `cancelling` - The user pulled down the refresher and let go, but did not pull down far enough to kick off the `refreshing` state. After letting go, the refresher is in the `cancelling` state while it is closing, and will go back to the `inactive` state once closed.
|
||||||
|
* - `ready` - The user has pulled down the refresher far enough that if they let go, it'll begin the `refreshing` state.
|
||||||
|
* - `refreshing` - The refresher is actively waiting on the async operation to end. Once the refresh handler calls `endRefreshing()` it will begin the `ending` state.
|
||||||
|
* - `ending` - The `refreshing` state has finished and the refresher is in the process of closing itself. Once closed, the refresher will go back to the `inactive` state.
|
||||||
|
*/
|
||||||
|
state: string = STATE_INACTIVE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Y coordinate of where the user started to the pull down the content.
|
||||||
|
*/
|
||||||
|
startY: number = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current touch or mouse event's Y coordinate.
|
||||||
|
*/
|
||||||
|
currentY: number = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The distance between the start of the pull and the current touch or
|
||||||
|
* mouse event's Y coordinate.
|
||||||
|
*/
|
||||||
|
deltaY: number = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A number representing how far down the user has pulled.
|
||||||
|
* The number `0` represents the user hasn't pulled down at all. The
|
||||||
|
* number `1`, and anything greater than `1`, represents that the user
|
||||||
|
* has pulled far enough down that when they let go then the refresh will
|
||||||
|
* happen. If they let go and the number is less than `1`, then the
|
||||||
|
* refresh will not happen, and the content will return to it's original
|
||||||
|
* position.
|
||||||
|
*/
|
||||||
|
progress: number = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @input {number} The min distance the user must pull down until the
|
||||||
|
* refresher can go into the `refreshing` state. Default is `60`.
|
||||||
|
*/
|
||||||
|
@Input() pullMin: number = 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @input {number} The maximum distance of the pull until the refresher
|
||||||
|
* will automatically go into the `refreshing` state. By default, the pull
|
||||||
|
* maximum will be the result of `pullMin + 60`.
|
||||||
|
*/
|
||||||
|
@Input() pullMax: number = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @input {number} How many milliseconds it takes to close the refresher. Default is `280`.
|
||||||
|
*/
|
||||||
|
@Input() closeDuration: number = 280;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @input {number} How many milliseconds it takes the refresher to to snap back to the `refreshing` state. Default is `280`.
|
||||||
|
*/
|
||||||
|
@Input() snapbackDuration: number = 280;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @input {boolean} If the refresher is enabled or not. Default is `true`.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
get enabled(): boolean {
|
||||||
|
return this._isEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
set enabled(val: boolean) {
|
||||||
|
this._isEnabled = isTrueProperty(val);
|
||||||
|
this._setListeners(this._isEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @output {event} When the user lets go and has pulled down far enough, which would be
|
||||||
|
* farther than the `pullMin`, then your refresh hander if fired and the state is
|
||||||
|
* updated to `refreshing`. From within your refresh handler, you must call the
|
||||||
|
* `endRefreshing()` method when your async operation has completed.
|
||||||
|
*/
|
||||||
|
@Output() refresh: EventEmitter<Refresher> = new EventEmitter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @output {event} While the user is pulling down the content and exposing the refresher.
|
||||||
|
*/
|
||||||
|
@Output() pulling: EventEmitter<Refresher> = new EventEmitter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @output {event} When the user begins to start pulling down.
|
||||||
|
*/
|
||||||
|
@Output() start: EventEmitter<Refresher> = new EventEmitter();
|
||||||
|
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Host() private _content: Content,
|
||||||
|
private _zone: NgZone,
|
||||||
|
elementRef: ElementRef
|
||||||
|
) {
|
||||||
|
_content.addCssClass('has-refresher');
|
||||||
|
|
||||||
|
// deprecated warning
|
||||||
|
let ele = elementRef.nativeElement;
|
||||||
|
let deprecatedAttrs = ['pullingIcon', 'pullingText', 'refreshingIcon', 'refreshingText', 'spinner'];
|
||||||
|
deprecatedAttrs.forEach(attrName => {
|
||||||
|
if (ele.hasAttribute(attrName)) {
|
||||||
|
console.warn('<ion-refresher> property "' + attrName + '" should now be placed on the inner <ion-refresher-content> component instead of <ion-refresher>. Please review the Refresher docs for API updates.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onStart(ev: TouchEvent): any {
|
||||||
|
// if multitouch then get out immediately
|
||||||
|
if (ev.touches && ev.touches.length > 1) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let coord = pointerCoord(ev);
|
||||||
|
console.debug('Pull-to-refresh, onStart', ev.type, 'y:', coord.y);
|
||||||
|
|
||||||
|
let now = Date.now();
|
||||||
|
if (this._lastStart + 100 > now) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
this._lastStart = now;
|
||||||
|
|
||||||
|
if ( ev.type === 'mousedown' && !this._mMove) {
|
||||||
|
this._mMove = this._content.addMouseMoveListener( this._onMove.bind(this) );
|
||||||
|
}
|
||||||
|
|
||||||
|
this.startY = this.currentY = coord.y;
|
||||||
|
this.progress = 0;
|
||||||
|
|
||||||
|
if (!this.pullMax) {
|
||||||
|
this.pullMax = (this.pullMin + 60);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onMove(ev: TouchEvent): any {
|
||||||
|
// this method can get called like a bazillion times per second,
|
||||||
|
// so it's built to be as efficient as possible, and does its
|
||||||
|
// best to do any DOM read/writes only when absolutely necessary
|
||||||
|
console.debug('Pull-to-refresh, onMove', ev.type);
|
||||||
|
|
||||||
|
// if multitouch then get out immediately
|
||||||
|
if (ev.touches && ev.touches.length > 1) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// do nothing if it's actively refreshing
|
||||||
|
// or it's in the process of closing
|
||||||
|
// or this was never a startY
|
||||||
|
if (this.startY === null || this.state === STATE_REFRESHING || this.state === STATE_CANCELLING || this.state === STATE_ENDING) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we just updated stuff less than 16ms ago
|
||||||
|
// then don't check again, just chillout plz
|
||||||
|
let now = Date.now();
|
||||||
|
if (this._lastCheck + 16 > now) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remember the last time we checked all this
|
||||||
|
this._lastCheck = now;
|
||||||
|
|
||||||
|
// get the current pointer coordinates
|
||||||
|
let coord = pointerCoord(ev);
|
||||||
|
|
||||||
|
this.currentY = coord.y;
|
||||||
|
|
||||||
|
// it's now possible they could be pulling down the content
|
||||||
|
// how far have they pulled so far?
|
||||||
|
this.deltaY = (coord.y - this.startY);
|
||||||
|
|
||||||
|
// don't bother if they're scrolling up
|
||||||
|
// and have not already started dragging
|
||||||
|
if (this.deltaY <= 0) {
|
||||||
|
// the current Y is higher than the starting Y
|
||||||
|
// so they scrolled up enough to be ignored
|
||||||
|
this.progress = 0;
|
||||||
|
|
||||||
|
if (this.state !== STATE_INACTIVE) {
|
||||||
|
this._zone.run(() => {
|
||||||
|
this.state = STATE_INACTIVE;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._appliedStyles) {
|
||||||
|
// reset the styles only if they were applied
|
||||||
|
this._setCss(0, '', false, '');
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state === STATE_INACTIVE) {
|
||||||
|
// this refresh is not alreadying actively pulling down
|
||||||
|
|
||||||
|
// get the content's scrollTop
|
||||||
|
let scrollHostScrollTop = this._content.getScrollTop();
|
||||||
|
|
||||||
|
// if the scrollTop is greater than zero then it's
|
||||||
|
// not possible to pull the content down yet
|
||||||
|
if (scrollHostScrollTop > 0) {
|
||||||
|
this.progress = 0;
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// content scrolled all the way to the top, and dragging down
|
||||||
|
this.state = STATE_PULLING;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prevent native scroll events
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
// the refresher is actively pulling at this point
|
||||||
|
// move the scroll element within the content element
|
||||||
|
this._setCss(this.deltaY, '0ms', true, '');
|
||||||
|
|
||||||
|
if (!this.deltaY) {
|
||||||
|
// don't continue if there's no delta yet
|
||||||
|
this.progress = 0;
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// so far so good, let's run this all back within zone now
|
||||||
|
this._zone.run(() => {
|
||||||
|
this._onMoveInZone();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onMoveInZone() {
|
||||||
|
// set pull progress
|
||||||
|
this.progress = (this.deltaY / this.pullMin);
|
||||||
|
|
||||||
|
// emit "start" if it hasn't started yet
|
||||||
|
if (!this._didStart) {
|
||||||
|
this._didStart = true;
|
||||||
|
this.start.emit(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit "pulling" on every move
|
||||||
|
this.pulling.emit(this);
|
||||||
|
|
||||||
|
// do nothing if the delta is less than the pull threshold
|
||||||
|
if (this.deltaY < this.pullMin) {
|
||||||
|
// ensure it stays in the pulling state, cuz its not ready yet
|
||||||
|
this.state = STATE_PULLING;
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.deltaY > this.pullMax) {
|
||||||
|
// they pulled farther than the max, so kick off the refresh
|
||||||
|
this._beginRefresh();
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pulled farther than the pull min!!
|
||||||
|
// it is now in the `ready` state!!
|
||||||
|
// if they let go then it'll refresh, kerpow!!
|
||||||
|
this.state = STATE_READY;
|
||||||
|
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onEnd(ev) {
|
||||||
|
// only run in a zone when absolutely necessary
|
||||||
|
|
||||||
|
if (this.state === STATE_READY) {
|
||||||
|
this._zone.run(() => {
|
||||||
|
// they pulled down far enough, so it's ready to refresh
|
||||||
|
this._beginRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
} else if (this.state === STATE_PULLING) {
|
||||||
|
this._zone.run(() => {
|
||||||
|
// they were pulling down, but didn't pull down far enough
|
||||||
|
// set the content back to it's original location
|
||||||
|
// and close the refresher
|
||||||
|
// set that the refresh is actively cancelling
|
||||||
|
this.cancelRefreshing();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset on any touchend/mouseup
|
||||||
|
this.startY = null;
|
||||||
|
if (this._mMove) {
|
||||||
|
// we don't want to always listen to mousemoves
|
||||||
|
// remove it if we're still listening
|
||||||
|
this._mMove();
|
||||||
|
this._mMove = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _beginRefresh() {
|
||||||
|
// assumes we're already back in a zone
|
||||||
|
// they pulled down far enough, so it's ready to refresh
|
||||||
|
this.state = STATE_REFRESHING;
|
||||||
|
|
||||||
|
// place the content in a hangout position while it thinks
|
||||||
|
this._setCss(this.pullMin,( this.snapbackDuration + 'ms'), true, '');
|
||||||
|
|
||||||
|
// emit "refresh" because it was pulled down far enough
|
||||||
|
// and they let go to begin refreshing
|
||||||
|
this.refresh.emit(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call `endRefreshing()` when your async operation has completed.
|
||||||
|
* For example, the `refreshing` state is while the app is performing
|
||||||
|
* an asynchronous operation, such as receiving more data from an
|
||||||
|
* AJAX request. Once the data has been received, you then call this
|
||||||
|
* method to signify that the refreshing has completed and to close
|
||||||
|
* the refresher. This method also changes the refresher's state from
|
||||||
|
* `refreshing` to `ending`.
|
||||||
|
*/
|
||||||
|
endRefreshing() {
|
||||||
|
this._close(STATE_ENDING, '120ms');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the refresher's state from `refreshing` to `cancelling`.
|
||||||
|
*/
|
||||||
|
cancelRefreshing() {
|
||||||
|
this._close(STATE_CANCELLING, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private complete() {
|
||||||
|
// deprecated warning
|
||||||
|
console.warn('refresher completed() deprecated, please update to endRefreshing()');
|
||||||
|
this.endRefreshing();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _close(state: string, delay: string) {
|
||||||
|
var timer;
|
||||||
|
|
||||||
|
function close(ev) {
|
||||||
|
// closing is done, return to inactive state
|
||||||
|
if (ev) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = STATE_INACTIVE;
|
||||||
|
this.progress = 0;
|
||||||
|
this._didStart = this.startY = this.currentY = this.deltaY = null;
|
||||||
|
this._setCss(0, '0ms', false, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// create fallback timer incase something goes wrong with transitionEnd event
|
||||||
|
timer = setTimeout(close.bind(this), 600);
|
||||||
|
|
||||||
|
// create transition end event on the content's scroll element
|
||||||
|
this._content.onScrollElementTransitionEnd(close.bind(this));
|
||||||
|
|
||||||
|
// reset set the styles on the scroll element
|
||||||
|
// set that the refresh is actively cancelling/completing
|
||||||
|
this.state = state;
|
||||||
|
this._setCss(0, '', true, delay);
|
||||||
|
|
||||||
|
if (this._mMove) {
|
||||||
|
// always remove the mousemove event
|
||||||
|
this._mMove();
|
||||||
|
this._mMove = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setCss(y: number, duration: string, overflowVisible: boolean, delay: string) {
|
||||||
|
this._appliedStyles = (y > 0);
|
||||||
|
|
||||||
|
var content = this._content;
|
||||||
|
content.setScrollElementStyle(CSS.transform, ((y > 0) ? 'translateY(' + y + 'px) translateZ(0px)' : 'translateZ(0px)'));
|
||||||
|
content.setScrollElementStyle(CSS.transitionDuration, duration);
|
||||||
|
content.setScrollElementStyle(CSS.transitionDelay, delay);
|
||||||
|
content.setScrollElementStyle('overflow', (overflowVisible ? 'hidden' : ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setListeners(shouldListen: boolean) {
|
||||||
|
const self = this;
|
||||||
|
const content = self._content;
|
||||||
|
|
||||||
|
if (shouldListen) {
|
||||||
|
// add listener outside of zone
|
||||||
|
// touch handlers
|
||||||
|
self._zone.runOutsideAngular(function() {
|
||||||
|
if (!self._tStart) {
|
||||||
|
self._tStart = content.addTouchStartListener( self._onStart.bind(self) );
|
||||||
|
}
|
||||||
|
if (!self._tMove) {
|
||||||
|
self._tMove = content.addTouchMoveListener( self._onMove.bind(self) );
|
||||||
|
}
|
||||||
|
if (!self._tEnd) {
|
||||||
|
self._tEnd = content.addTouchEndListener( self._onEnd.bind(self) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// mouse handlers
|
||||||
|
// mousemove does not get added until mousedown fires
|
||||||
|
if (!self._mDown) {
|
||||||
|
self._mDown = content.addMouseDownListener( self._onStart.bind(self) );
|
||||||
|
}
|
||||||
|
if (!self._mUp) {
|
||||||
|
self._mUp = content.addMouseUpListener( self._onEnd.bind(self) );
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// unregister event listeners from content element
|
||||||
|
self._mDown && self._mDown();
|
||||||
|
self._mMove && self._mMove();
|
||||||
|
self._mUp && self._mUp();
|
||||||
|
self._tStart && self._tStart();
|
||||||
|
self._tMove && self._tMove();
|
||||||
|
self._tEnd && self._tEnd();
|
||||||
|
|
||||||
|
self._mDown = self._mMove = self._mUp = self._tStart = self._tMove = self._tEnd = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ngOnInit() {
|
||||||
|
// bind event listeners
|
||||||
|
// save the unregister listener functions to use onDestroy
|
||||||
|
this._setListeners(this._isEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ngOnDestroy() {
|
||||||
|
this._setListeners(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATE_INACTIVE = 'inactive';
|
||||||
|
const STATE_PULLING = 'pulling';
|
||||||
|
const STATE_READY = 'ready';
|
||||||
|
const STATE_REFRESHING = 'refreshing';
|
||||||
|
const STATE_CANCELLING = 'cancelling';
|
||||||
|
const STATE_ENDING = 'ending';
|
85
ionic/components/refresher/test/basic/index.ts
Normal file
85
ionic/components/refresher/test/basic/index.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import {App} from 'ionic-angular';
|
||||||
|
|
||||||
|
|
||||||
|
@App({
|
||||||
|
templateUrl: 'main.html'
|
||||||
|
})
|
||||||
|
class E2EApp {
|
||||||
|
items = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
this.items.push( getRandomData() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doRefresh(refresher) {
|
||||||
|
console.log('Begin async operation');
|
||||||
|
|
||||||
|
getAsyncData().then(newData => {
|
||||||
|
for (var i = 0; i < newData.length; i++) {
|
||||||
|
this.items.unshift( newData[i] );
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Finished receiving data, async operation complete');
|
||||||
|
refresher.endRefreshing();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
doStart(refresher) {
|
||||||
|
console.log('Refresher, start');
|
||||||
|
}
|
||||||
|
|
||||||
|
doPulling(refresher) {
|
||||||
|
console.log('Pulling', refresher.progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAsyncData() {
|
||||||
|
// async return mock data
|
||||||
|
return new Promise(resolve => {
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
let data = [];
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
data.push( getRandomData() );
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(data);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomData() {
|
||||||
|
let i = Math.floor( Math.random() * data.length );
|
||||||
|
return data[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
'Fast Times at Ridgemont High',
|
||||||
|
'Peggy Sue Got Married',
|
||||||
|
'Raising Arizona',
|
||||||
|
'Moonstruck',
|
||||||
|
'Fire Birds',
|
||||||
|
'Honeymoon in Vegas',
|
||||||
|
'Amos & Andrew',
|
||||||
|
'It Could Happen to You',
|
||||||
|
'Trapped in Paradise',
|
||||||
|
'Leaving Las Vegas',
|
||||||
|
'The Rock',
|
||||||
|
'Con Air',
|
||||||
|
'Face/Off',
|
||||||
|
'City of Angels',
|
||||||
|
'Gone in Sixty Seconds',
|
||||||
|
'The Family Man',
|
||||||
|
'Windtalkers',
|
||||||
|
'Matchstick Men',
|
||||||
|
'National Treasure',
|
||||||
|
'Ghost Rider',
|
||||||
|
'Grindhouse',
|
||||||
|
'Next',
|
||||||
|
'Kick-Ass',
|
||||||
|
'Drive Angry'
|
||||||
|
];
|
21
ionic/components/refresher/test/basic/main.html
Normal file
21
ionic/components/refresher/test/basic/main.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<ion-toolbar><ion-title>Pull To Refresh</ion-title></ion-toolbar>
|
||||||
|
|
||||||
|
<ion-content>
|
||||||
|
|
||||||
|
<ion-refresher (start)="doStart($event)" (pulling)="doPulling($event)" (refresh)="doRefresh($event)">
|
||||||
|
|
||||||
|
<ion-refresher-content
|
||||||
|
pullingText="Pull to refresh..."
|
||||||
|
refreshingSpinner="bubbles"
|
||||||
|
refreshingText="Refreshing...">
|
||||||
|
</ion-refresher-content>
|
||||||
|
|
||||||
|
</ion-refresher>
|
||||||
|
|
||||||
|
<ion-list>
|
||||||
|
<ion-item *ngFor="#item of items">
|
||||||
|
{{ item }}
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
|
||||||
|
</ion-content>
|
257
ionic/components/refresher/test/refresher.spec.ts
Normal file
257
ionic/components/refresher/test/refresher.spec.ts
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
import {Refresher, Content, Config, Ion} from 'ionic-angular';
|
||||||
|
|
||||||
|
export function run() {
|
||||||
|
|
||||||
|
describe('Refresher', () => {
|
||||||
|
|
||||||
|
describe('_onEnd', () => {
|
||||||
|
|
||||||
|
it('should set to refreshing if state=ready', () => {
|
||||||
|
refresher.state = 'ready';
|
||||||
|
refresher._onEnd();
|
||||||
|
expect(refresher.state).toEqual('refreshing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set to canelling if state=pulling on release', () => {
|
||||||
|
refresher.state = 'pulling';
|
||||||
|
refresher._onEnd();
|
||||||
|
expect(refresher.state).toEqual('cancelling');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing if state=cancelling', () => {
|
||||||
|
refresher.state = 'cancelling';
|
||||||
|
var results = refresher._onEnd();
|
||||||
|
expect(refresher.state).toEqual('cancelling');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing if state=completing', () => {
|
||||||
|
refresher.state = 'completing';
|
||||||
|
var results = refresher._onEnd();
|
||||||
|
expect(refresher.state).toEqual('completing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing if state=refreshing', () => {
|
||||||
|
refresher.state = 'refreshing';
|
||||||
|
var results = refresher._onEnd();
|
||||||
|
expect(refresher.state).toEqual('refreshing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing if state=inactive', () => {
|
||||||
|
refresher.state = 'inactive';
|
||||||
|
refresher._onEnd();
|
||||||
|
expect(refresher.state).toEqual('inactive');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('_onMoveInZone', () => {
|
||||||
|
|
||||||
|
it('should set ready state when pulling down and it went past the pull min', () => {
|
||||||
|
refresher.state = 'inactive';
|
||||||
|
|
||||||
|
refresher.pullMin = 100;
|
||||||
|
refresher.pullMax = 200;
|
||||||
|
refresher.deltaY = 100;
|
||||||
|
let result = refresher._onMoveInZone();
|
||||||
|
|
||||||
|
expect(result).toEqual(4);
|
||||||
|
expect(refresher.state).toEqual('ready');
|
||||||
|
expect(refresher.progress).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set begin refreshing when pulling down and it went past the pull max', () => {
|
||||||
|
refresher.state = 'inactive';
|
||||||
|
|
||||||
|
refresher.pullMin = 100;
|
||||||
|
refresher.pullMax = 200;
|
||||||
|
refresher.deltaY = 250;
|
||||||
|
let result = refresher._onMoveInZone();
|
||||||
|
|
||||||
|
expect(result).toEqual(3);
|
||||||
|
expect(refresher.state).toEqual('refreshing');
|
||||||
|
expect(refresher.progress).toEqual(2.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set pulling state when pulling down, but not past the pull min', () => {
|
||||||
|
refresher.state = 'inactive';
|
||||||
|
|
||||||
|
refresher.pullMin = 100;
|
||||||
|
refresher.pullMax = 200;
|
||||||
|
refresher.deltaY = 50;
|
||||||
|
let result = refresher._onMoveInZone();
|
||||||
|
|
||||||
|
expect(result).toEqual(2);
|
||||||
|
expect(refresher.state).toEqual('pulling');
|
||||||
|
expect(refresher.progress).toEqual(0.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('_onMove', () => {
|
||||||
|
|
||||||
|
it('should set scrollElement inline styles when pulling down, but not past threshold', () => {
|
||||||
|
setContentScrollTop(0);
|
||||||
|
refresher.startY = 100;
|
||||||
|
refresher.pullMin = 80;
|
||||||
|
let result = refresher._onMove( touchEv(125) );
|
||||||
|
|
||||||
|
expect(getScrollElementStyles().transform).toEqual('translateY(25px) translateZ(0px)');
|
||||||
|
expect(getScrollElementStyles().transitionDuration).toEqual('0ms');
|
||||||
|
expect(getScrollElementStyles().overflow).toEqual('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set scrollElement inline styles when pulling up above startY', () => {
|
||||||
|
refresher.state = 'inactive';
|
||||||
|
refresher._appliedStyles = false;
|
||||||
|
|
||||||
|
setContentScrollTop(1);
|
||||||
|
refresher.startY = 100;
|
||||||
|
let result = refresher._onMove( touchEv(95) );
|
||||||
|
|
||||||
|
expect(result).toEqual(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not pull when scrolling down, state=inactive, deltaY>0, scrollTop>0', () => {
|
||||||
|
refresher.state = 'inactive';
|
||||||
|
|
||||||
|
setContentScrollTop(50);
|
||||||
|
refresher.startY = 100;
|
||||||
|
let result = refresher._onMove( touchEv(125) );
|
||||||
|
|
||||||
|
expect(refresher.state).toEqual('inactive');
|
||||||
|
expect(result).toEqual(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset styles when _appliedStyles=true, delta<=0', () => {
|
||||||
|
refresher._appliedStyles = true;
|
||||||
|
|
||||||
|
refresher.startY = 100;
|
||||||
|
let result = refresher._onMove( touchEv(85) );
|
||||||
|
|
||||||
|
expect(refresher.state).toEqual('inactive');
|
||||||
|
expect(getScrollElementStyles().transform).toEqual('translateZ(0px)');
|
||||||
|
expect(getScrollElementStyles().transitionDuration).toEqual('');
|
||||||
|
expect(getScrollElementStyles().overflow).toEqual('');
|
||||||
|
expect(result).toEqual(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not run when scrollTop is > 0', () => {
|
||||||
|
setContentScrollTop(50);
|
||||||
|
refresher.startY = 100;
|
||||||
|
|
||||||
|
var results = refresher._onMove(touchEv(80));
|
||||||
|
expect(results).toEqual(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not run when scrolling up, but isnt actively dragging', () => {
|
||||||
|
setContentScrollTop(1);
|
||||||
|
refresher.startY = 100;
|
||||||
|
refresher._isDragging = false
|
||||||
|
|
||||||
|
var results = refresher._onMove(touchEv(85));
|
||||||
|
expect(results).toEqual(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the deltaY', () => {
|
||||||
|
refresher.startY = 100;
|
||||||
|
refresher._onMove( touchEv(133) );
|
||||||
|
expect(refresher.deltaY).toEqual(33);
|
||||||
|
|
||||||
|
refresher._lastCheck = 0; // force allow next check
|
||||||
|
|
||||||
|
refresher._onMove( touchEv(50) );
|
||||||
|
expect(refresher.deltaY).toEqual(-50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not run if it already ran less than 16ms ago', () => {
|
||||||
|
refresher.startY = 100;
|
||||||
|
var results = refresher._onMove(touchEv(88));
|
||||||
|
expect(results).toEqual(6);
|
||||||
|
|
||||||
|
results = refresher._onMove(touchEv(88));
|
||||||
|
expect(results).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not run if state=refreshing', () => {
|
||||||
|
refresher.startY = 100;
|
||||||
|
refresher.state = 'refreshing';
|
||||||
|
var results = refresher._onMove( touchEv(88) );
|
||||||
|
expect(results).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not run if state=ending', () => {
|
||||||
|
refresher.startY = 100;
|
||||||
|
refresher.state = 'ending';
|
||||||
|
var results = refresher._onMove( touchEv(88) );
|
||||||
|
expect(results).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not run if state=cancelling', () => {
|
||||||
|
refresher.startY = 100;
|
||||||
|
refresher.state = 'cancelling';
|
||||||
|
var results = refresher._onMove( touchEv(88) );
|
||||||
|
expect(results).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not run if no startY', () => {
|
||||||
|
refresher.startY = null;
|
||||||
|
var results = refresher._onMove( touchEv(88) );
|
||||||
|
expect(results).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not run if multiple touches', () => {
|
||||||
|
var results = refresher._onMove({
|
||||||
|
touches: [{},{}]
|
||||||
|
});
|
||||||
|
expect(results).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
let config = new Config();
|
||||||
|
let refresher: Refresher;
|
||||||
|
let content: Content;
|
||||||
|
let contentElementRef;
|
||||||
|
let zone = {
|
||||||
|
run: function(cb) {cb()},
|
||||||
|
runOutsideAngular: function(cb) {cb()}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
contentElementRef = mockElementRef();
|
||||||
|
content = new Content(contentElementRef, config, null, null, null);
|
||||||
|
content.scrollElement = document.createElement('scroll-content');
|
||||||
|
|
||||||
|
refresher = new Refresher(content, zone, mockElementRef());
|
||||||
|
});
|
||||||
|
|
||||||
|
function touchEv(y: number) {
|
||||||
|
return {
|
||||||
|
type: 'mockTouch',
|
||||||
|
touches: [{clientY: y}],
|
||||||
|
preventDefault: function(){}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockElementRef() {
|
||||||
|
return {
|
||||||
|
nativeElement: {
|
||||||
|
classList: { add: function(){}, remove: function(){} },
|
||||||
|
scrollTop: 0,
|
||||||
|
hasAttribute: function(){}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setContentScrollTop(scrollTop) {
|
||||||
|
contentElementRef.nativeElement.scrollTop = scrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScrollElementStyles() {
|
||||||
|
return content.scrollElement.style;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
@ -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>
|
|
@ -10,7 +10,8 @@ import {Button} from '../components/button/button';
|
|||||||
import {Blur} from '../components/blur/blur';
|
import {Blur} from '../components/blur/blur';
|
||||||
import {Content} from '../components/content/content';
|
import {Content} from '../components/content/content';
|
||||||
import {Scroll} from '../components/scroll/scroll';
|
import {Scroll} from '../components/scroll/scroll';
|
||||||
import {Refresher} from '../components/scroll/pull-to-refresh';
|
import {Refresher} from '../components/refresher/refresher';
|
||||||
|
import {RefresherContent} from '../components/refresher/refresher-content';
|
||||||
import {Slides, Slide, SlideLazy} from '../components/slides/slides';
|
import {Slides, Slide, SlideLazy} from '../components/slides/slides';
|
||||||
import {Tabs} from '../components/tabs/tabs';
|
import {Tabs} from '../components/tabs/tabs';
|
||||||
import {Tab} from '../components/tabs/tab';
|
import {Tab} from '../components/tabs/tab';
|
||||||
@ -58,6 +59,7 @@ import {ShowWhen, HideWhen} from '../components/show-hide-when/show-hide-when';
|
|||||||
* - Content
|
* - Content
|
||||||
* - Scroll
|
* - Scroll
|
||||||
* - Refresher
|
* - Refresher
|
||||||
|
* - RefresherContent
|
||||||
*
|
*
|
||||||
* **Lists**
|
* **Lists**
|
||||||
* - List
|
* - List
|
||||||
@ -125,6 +127,7 @@ export const IONIC_DIRECTIVES = [
|
|||||||
Content,
|
Content,
|
||||||
Scroll,
|
Scroll,
|
||||||
Refresher,
|
Refresher,
|
||||||
|
RefresherContent,
|
||||||
|
|
||||||
// Lists
|
// Lists
|
||||||
List,
|
List,
|
||||||
|
@ -45,6 +45,7 @@ export let CSS: {
|
|||||||
transform?: string,
|
transform?: string,
|
||||||
transition?: string,
|
transition?: string,
|
||||||
transitionDuration?: string,
|
transitionDuration?: string,
|
||||||
|
transitionDelay?: string,
|
||||||
transitionTimingFn?: string,
|
transitionTimingFn?: string,
|
||||||
transitionStart?: string,
|
transitionStart?: string,
|
||||||
transitionEnd?: string,
|
transitionEnd?: string,
|
||||||
@ -80,6 +81,9 @@ export let CSS: {
|
|||||||
// transition timing function
|
// transition timing function
|
||||||
CSS.transitionTimingFn = (isWebkit ? '-webkit-' : '') + 'transition-timing-function';
|
CSS.transitionTimingFn = (isWebkit ? '-webkit-' : '') + 'transition-timing-function';
|
||||||
|
|
||||||
|
// transition delay
|
||||||
|
CSS.transitionDelay = (isWebkit ? '-webkit-' : '') + 'transition-delay';
|
||||||
|
|
||||||
// To be sure transitionend works everywhere, include *both* the webkit and non-webkit events
|
// To be sure transitionend works everywhere, include *both* the webkit and non-webkit events
|
||||||
CSS.transitionEnd = (isWebkit ? 'webkitTransitionEnd ' : '') + 'transitionend';
|
CSS.transitionEnd = (isWebkit ? 'webkitTransitionEnd ' : '') + 'transitionend';
|
||||||
})();
|
})();
|
||||||
@ -156,7 +160,7 @@ export function windowLoad(callback?: Function) {
|
|||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pointerCoord(ev): {x: number, y: number} {
|
export function pointerCoord(ev: any): {x: number, y: number} {
|
||||||
// get coordinates for either a mouse click
|
// get coordinates for either a mouse click
|
||||||
// or a touch depending on the given event
|
// or a touch depending on the given event
|
||||||
let c = { x: 0, y: 0 };
|
let c = { x: 0, y: 0 };
|
||||||
@ -243,7 +247,6 @@ export function closest(ele: HTMLElement, selector: string, checkSelf?: boolean)
|
|||||||
/**
|
/**
|
||||||
* Get the element offsetWidth and offsetHeight. Values are cached
|
* Get the element offsetWidth and offsetHeight. Values are cached
|
||||||
* to reduce DOM reads. Cache is cleared on a window resize.
|
* to reduce DOM reads. Cache is cleared on a window resize.
|
||||||
* @param {TODO} ele TODO
|
|
||||||
*/
|
*/
|
||||||
export function getDimensions(ele: HTMLElement, id: string): {
|
export function getDimensions(ele: HTMLElement, id: string): {
|
||||||
width: number, height: number, left: number, top: number
|
width: number, height: number, left: number, top: number
|
||||||
@ -268,6 +271,10 @@ export function getDimensions(ele: HTMLElement, id: string): {
|
|||||||
return dimensions;
|
return dimensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clearDimensions(id: string) {
|
||||||
|
delete dimensionCache[id];
|
||||||
|
}
|
||||||
|
|
||||||
export function windowDimensions(): {width: number, height: number} {
|
export function windowDimensions(): {width: number, height: number} {
|
||||||
if (!dimensionCache.win) {
|
if (!dimensionCache.win) {
|
||||||
// make sure we got good values before caching
|
// make sure we got good values before caching
|
||||||
|
Reference in New Issue
Block a user