mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
feat(refresher): adds refresher
This commit is contained in:
68
packages/core/src/components.d.ts
vendored
68
packages/core/src/components.d.ts
vendored
@@ -993,6 +993,7 @@ declare global {
|
||||
onPress?: GestureCallback;
|
||||
onStart?: GestureCallback;
|
||||
onWillStart?: (_: GestureDetail) => Promise<void>;
|
||||
passive?: boolean;
|
||||
threshold?: number;
|
||||
type?: string;
|
||||
}
|
||||
@@ -2143,6 +2144,73 @@ declare global {
|
||||
}
|
||||
|
||||
|
||||
import {
|
||||
RefresherContent as IonRefresherContent
|
||||
} from './components/refresher-content/refresher-content';
|
||||
|
||||
declare global {
|
||||
interface HTMLIonRefresherContentElement extends IonRefresherContent, HTMLElement {
|
||||
}
|
||||
var HTMLIonRefresherContentElement: {
|
||||
prototype: HTMLIonRefresherContentElement;
|
||||
new (): HTMLIonRefresherContentElement;
|
||||
};
|
||||
interface HTMLElementTagNameMap {
|
||||
"ion-refresher-content": HTMLIonRefresherContentElement;
|
||||
}
|
||||
interface ElementTagNameMap {
|
||||
"ion-refresher-content": HTMLIonRefresherContentElement;
|
||||
}
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
"ion-refresher-content": JSXElements.IonRefresherContentAttributes;
|
||||
}
|
||||
}
|
||||
namespace JSXElements {
|
||||
export interface IonRefresherContentAttributes extends HTMLAttributes {
|
||||
pullingIcon?: string;
|
||||
pullingText?: string;
|
||||
refreshingSpinner?: string;
|
||||
refreshingText?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
import {
|
||||
Refresher as IonRefresher
|
||||
} from './components/refresher/refresher';
|
||||
|
||||
declare global {
|
||||
interface HTMLIonRefresherElement extends IonRefresher, HTMLElement {
|
||||
}
|
||||
var HTMLIonRefresherElement: {
|
||||
prototype: HTMLIonRefresherElement;
|
||||
new (): HTMLIonRefresherElement;
|
||||
};
|
||||
interface HTMLElementTagNameMap {
|
||||
"ion-refresher": HTMLIonRefresherElement;
|
||||
}
|
||||
interface ElementTagNameMap {
|
||||
"ion-refresher": HTMLIonRefresherElement;
|
||||
}
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
"ion-refresher": JSXElements.IonRefresherAttributes;
|
||||
}
|
||||
}
|
||||
namespace JSXElements {
|
||||
export interface IonRefresherAttributes extends HTMLAttributes {
|
||||
closeDuration?: string;
|
||||
enabled?: boolean;
|
||||
pullDelta?: number;
|
||||
pullMin?: number;
|
||||
snapbackDuration?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
import {
|
||||
ReorderGroup as IonReorderGroup
|
||||
} from './components/reorder-group/reorder-group';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, Element, Event, EventEmitter, Listen, Prop, Watch } from '@stencil/core';
|
||||
import { Component, Element, Event, EventEmitter, EventListenerEnable, Listen, Prop, Watch } from '@stencil/core';
|
||||
import { ElementRef, applyStyles, assert, getElementReference, updateDetail } from '../../utils/helpers';
|
||||
import { BLOCK_ALL, BlockerDelegate, GestureController, GestureDelegate } from '../gesture-controller/gesture-controller';
|
||||
import { DomController } from '../../index';
|
||||
@@ -28,7 +28,7 @@ export class Gesture {
|
||||
@Element() private el: HTMLElement;
|
||||
|
||||
@Prop({ context: 'dom' }) dom: DomController;
|
||||
@Prop({ context: 'enableListener' }) enableListener: any;
|
||||
@Prop({ context: 'enableListener' }) enableListener: EventListenerEnable;
|
||||
|
||||
@Prop() enabled = true;
|
||||
@Prop() attachTo: ElementRef = 'child';
|
||||
@@ -38,6 +38,7 @@ export class Gesture {
|
||||
@Prop() direction = 'x';
|
||||
@Prop() gestureName = '';
|
||||
@Prop() gesturePriority = 0;
|
||||
@Prop() passive = true;
|
||||
@Prop() maxAngle = 40;
|
||||
@Prop() threshold = 10;
|
||||
@Prop() type = 'pan';
|
||||
@@ -105,8 +106,8 @@ export class Gesture {
|
||||
@Watch('enabled')
|
||||
protected enabledChanged(isEnabled: boolean) {
|
||||
if (this.pan || this.hasPress) {
|
||||
this.enableListener(this, 'touchstart', isEnabled, this.attachTo);
|
||||
this.enableListener(this, 'mousedown', isEnabled, this.attachTo);
|
||||
this.enableListener(this, 'touchstart', isEnabled, this.attachTo, this.passive);
|
||||
this.enableListener(this, 'mousedown', isEnabled, this.attachTo, this.passive);
|
||||
if (!isEnabled) {
|
||||
this.abortGesture();
|
||||
}
|
||||
@@ -433,18 +434,18 @@ export class Gesture {
|
||||
|
||||
private enableMouse(shouldEnable: boolean) {
|
||||
if (this.pan) {
|
||||
this.enableListener(this, 'document:mousemove', shouldEnable);
|
||||
this.enableListener(this, 'document:mousemove', shouldEnable, undefined, this.passive);
|
||||
}
|
||||
this.enableListener(this, 'document:mouseup', shouldEnable);
|
||||
this.enableListener(this, 'document:mouseup', shouldEnable, undefined, this.passive);
|
||||
}
|
||||
|
||||
|
||||
private enableTouch(shouldEnable: boolean) {
|
||||
if (this.pan) {
|
||||
this.enableListener(this, 'touchmove', shouldEnable, this.attachTo);
|
||||
this.enableListener(this, 'touchmove', shouldEnable, this.attachTo, this.passive);
|
||||
}
|
||||
this.enableListener(this, 'touchcancel', shouldEnable, this.attachTo);
|
||||
this.enableListener(this, 'touchend', shouldEnable, this.attachTo);
|
||||
this.enableListener(this, 'touchcancel', shouldEnable, this.attachTo, this.passive);
|
||||
this.enableListener(this, 'touchend', shouldEnable, this.attachTo, this.passive);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -87,6 +87,11 @@ any
|
||||
any
|
||||
|
||||
|
||||
#### passive
|
||||
|
||||
boolean
|
||||
|
||||
|
||||
#### threshold
|
||||
|
||||
number
|
||||
@@ -179,6 +184,11 @@ any
|
||||
any
|
||||
|
||||
|
||||
#### passive
|
||||
|
||||
boolean
|
||||
|
||||
|
||||
#### threshold
|
||||
|
||||
number
|
||||
|
||||
55
packages/core/src/components/refresher-content/readme.md
Normal file
55
packages/core/src/components/refresher-content/readme.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# ion-refresher-content
|
||||
|
||||
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
#### pullingIcon
|
||||
|
||||
string
|
||||
|
||||
|
||||
#### pullingText
|
||||
|
||||
string
|
||||
|
||||
|
||||
#### refreshingSpinner
|
||||
|
||||
string
|
||||
|
||||
|
||||
#### refreshingText
|
||||
|
||||
string
|
||||
|
||||
|
||||
## Attributes
|
||||
|
||||
#### pullingIcon
|
||||
|
||||
string
|
||||
|
||||
|
||||
#### pullingText
|
||||
|
||||
string
|
||||
|
||||
|
||||
#### refreshingSpinner
|
||||
|
||||
string
|
||||
|
||||
|
||||
#### refreshingText
|
||||
|
||||
string
|
||||
|
||||
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built by [StencilJS](https://stenciljs.com/)*
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Component, Prop } from '@stencil/core';
|
||||
import { Config } from '../../index';
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
@Component({
|
||||
tag: 'ion-refresher-content'
|
||||
})
|
||||
export class RefresherContent {
|
||||
|
||||
@Prop({ context: 'config' }) config: Config;
|
||||
|
||||
/**
|
||||
* @input {string} a static icon to display when you begin to pull down
|
||||
*/
|
||||
@Prop({mutable: true}) pullingIcon: string;
|
||||
|
||||
/**
|
||||
* @input {string} the text you want to display when you begin to pull down
|
||||
*/
|
||||
@Prop() pullingText: string;
|
||||
|
||||
/**
|
||||
* @input {string} An animated SVG spinner that shows when refreshing begins
|
||||
*/
|
||||
@Prop({mutable: true}) refreshingSpinner: string;
|
||||
|
||||
/**
|
||||
* @input {string} the text you want to display when performing a refresh
|
||||
*/
|
||||
@Prop() refreshingText: string;
|
||||
|
||||
|
||||
protected componentDidLoad() {
|
||||
if (!this.pullingIcon) {
|
||||
this.pullingIcon = this.config.get('ionPullIcon', 'arrow-down');
|
||||
}
|
||||
if (!this.refreshingSpinner) {
|
||||
this.refreshingSpinner = this.config.get('ionRefreshingSpinner', this.config.get('spinner', 'lines'));
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return [
|
||||
<div class='refresher-pulling'>
|
||||
{this.pullingIcon &&
|
||||
<div class='refresher-pulling-icon'>
|
||||
<ion-icon name={this.pullingIcon}></ion-icon>
|
||||
</div>
|
||||
}
|
||||
{this.pullingText &&
|
||||
<div class='refresher-pulling-text' innerHTML={this.pullingText}></div>
|
||||
}
|
||||
</div>,
|
||||
<div class='refresher-refreshing'>
|
||||
{this.refreshingSpinner &&
|
||||
<div class='refresher-refreshing-icon'>
|
||||
<ion-spinner name={this.refreshingSpinner}></ion-spinner>
|
||||
</div>
|
||||
}
|
||||
{this.refreshingText &&
|
||||
<div class='refresher-refreshing-text' innerHTML={this.refreshingText}></div>
|
||||
}
|
||||
</div>
|
||||
];
|
||||
}
|
||||
}
|
||||
105
packages/core/src/components/refresher/readme.md
Normal file
105
packages/core/src/components/refresher/readme.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# ion-refresher-content
|
||||
|
||||
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
#### closeDuration
|
||||
|
||||
string
|
||||
|
||||
|
||||
#### enabled
|
||||
|
||||
boolean
|
||||
|
||||
|
||||
#### pullDelta
|
||||
|
||||
number
|
||||
|
||||
|
||||
#### pullMin
|
||||
|
||||
number
|
||||
|
||||
|
||||
#### snapbackDuration
|
||||
|
||||
string
|
||||
|
||||
|
||||
## Attributes
|
||||
|
||||
#### closeDuration
|
||||
|
||||
string
|
||||
|
||||
|
||||
#### enabled
|
||||
|
||||
boolean
|
||||
|
||||
|
||||
#### pullDelta
|
||||
|
||||
number
|
||||
|
||||
|
||||
#### pullMin
|
||||
|
||||
number
|
||||
|
||||
|
||||
#### snapbackDuration
|
||||
|
||||
string
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
#### ionPull
|
||||
|
||||
|
||||
#### ionRefresh
|
||||
|
||||
|
||||
#### ionStart
|
||||
|
||||
|
||||
## Methods
|
||||
|
||||
#### cancel()
|
||||
|
||||
Changes the refresher's state from `refreshing` to `cancelling`.
|
||||
|
||||
|
||||
#### complete()
|
||||
|
||||
Call `complete()` 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 `completing`.
|
||||
|
||||
|
||||
#### getProgress()
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built by [StencilJS](https://stenciljs.com/)*
|
||||
146
packages/core/src/components/refresher/refresher.scss
Normal file
146
packages/core/src/components/refresher/refresher.scss
Normal file
@@ -0,0 +1,146 @@
|
||||
@import "../../themes/ionic.globals";
|
||||
|
||||
// Refresher
|
||||
// --------------------------------------------------
|
||||
|
||||
/// @prop - Height of the refresher
|
||||
$refresher-height: 60px !default;
|
||||
|
||||
/// @prop - Color of the refresher icon
|
||||
$refresher-icon-color: #000 !default;
|
||||
|
||||
/// @prop - Font size of the refresher icon
|
||||
$refresher-icon-font-size: 30px !default;
|
||||
|
||||
/// @prop - Text color of the refresher content
|
||||
$refresher-text-color: #000 !default;
|
||||
|
||||
/// @prop - Font size of the refresher content
|
||||
$refresher-text-font-size: 16px !default;
|
||||
|
||||
/// @prop - Border color of the refresher
|
||||
$refresher-border-color: #ddd !default;
|
||||
|
||||
|
||||
ion-refresher {
|
||||
@include position(0, null, null, 0);
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: $z-index-refresher;
|
||||
|
||||
display: none;
|
||||
|
||||
width: 100%;
|
||||
height: $refresher-height;
|
||||
|
||||
&.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
|
||||
@include margin(-1px, null, null, null);
|
||||
|
||||
border-top: 1px solid $refresher-border-color;
|
||||
transition: transform 320ms cubic-bezier(.36, .66, .04, 1);
|
||||
}
|
||||
|
||||
|
||||
// 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 {
|
||||
@include text-align(center);
|
||||
@include transform-origin(center);
|
||||
|
||||
font-size: $refresher-icon-font-size;
|
||||
color: $refresher-icon-color;
|
||||
transition: 200ms;
|
||||
}
|
||||
|
||||
.refresher-pulling-text,
|
||||
.refresher-refreshing-text {
|
||||
@include text-align(center);
|
||||
|
||||
font-size: $refresher-text-font-size;
|
||||
color: $refresher-text-color;
|
||||
}
|
||||
|
||||
.refresher-refreshing .spinner-ios line,
|
||||
.refresher-refreshing .spinner-ios-small line,
|
||||
.refresher-refreshing .spinner-crescent circle {
|
||||
stroke: $refresher-icon-color;
|
||||
}
|
||||
|
||||
.refresher-refreshing .spinner-bubbles circle,
|
||||
.refresher-refreshing .spinner-circles circle,
|
||||
.refresher-refreshing .spinner-dots circle {
|
||||
fill: $refresher-icon-color;
|
||||
}
|
||||
|
||||
|
||||
// Refresher Content States
|
||||
// --------------------------------------------------
|
||||
|
||||
.refresher-pulling ion-refresher-content {
|
||||
.refresher-pulling {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.refresher-ready ion-refresher-content {
|
||||
.refresher-pulling {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.refresher-pulling-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.refresher-refreshing ion-refresher-content {
|
||||
.refresher-refreshing {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.refresher-cancelling ion-refresher-content {
|
||||
.refresher-pulling {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.refresher-pulling-icon {
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
|
||||
.refresher-completing ion-refresher-content {
|
||||
.refresher-refreshing {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.refresher-refreshing-icon {
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
448
packages/core/src/components/refresher/refresher.tsx
Normal file
448
packages/core/src/components/refresher/refresher.tsx
Normal file
@@ -0,0 +1,448 @@
|
||||
import { Component, Element, Event, EventEmitter, Method, Prop, State } from '@stencil/core';
|
||||
import { DomController, GestureDetail } from '../../index';
|
||||
|
||||
const enum RefresherState {
|
||||
Inactive = 1 << 0,
|
||||
Pulling = 1 << 1,
|
||||
Ready = 1 << 2,
|
||||
Refreshing = 1 << 3,
|
||||
Cancelling = 1 << 4,
|
||||
Completing = 1 << 5,
|
||||
|
||||
_BUSY_ = Refreshing | Cancelling | Completing,
|
||||
}
|
||||
|
||||
/**
|
||||
* @name Refresher
|
||||
* @description
|
||||
* The Refresher provides pull-to-refresh functionality on a content component.
|
||||
* Place the `ion-refresher` as the first child of your `ion-content` element.
|
||||
*
|
||||
* Pages can then listen to the refresher's various output events. The
|
||||
* `refresh` output event is 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 `complete()`.
|
||||
*
|
||||
* Note: Do not wrap the `ion-refresher` in a `*ngIf`. It will not render
|
||||
* properly this way. Please use the `enabled` property instead to
|
||||
* display or hide the refresher.
|
||||
*
|
||||
* @usage
|
||||
* ```html
|
||||
* <ion-content>
|
||||
*
|
||||
* <ion-refresher (ionRefresh)="doRefresh($event)">
|
||||
* <ion-refresher-content></ion-refresher-content>
|
||||
* </ion-refresher>
|
||||
*
|
||||
* </ion-content>
|
||||
* ```
|
||||
*
|
||||
* ```ts
|
||||
* @Component({...})
|
||||
* export class NewsFeedPage {
|
||||
*
|
||||
* doRefresh(refresher) {
|
||||
* console.log('Begin async operation', refresher);
|
||||
*
|
||||
* setTimeout(() => {
|
||||
* console.log('Async operation has ended');
|
||||
* refresher.complete();
|
||||
* }, 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 (ionRefresh)="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-refresher` component holds the refresh logic.
|
||||
* It requires a child component in order to display the content.
|
||||
* Ionic uses `ion-refresher-content` by default. This component
|
||||
* displays the refresher and changes the look depending
|
||||
* on the refresher's state. Separating these components
|
||||
* allows developers to create their own refresher content
|
||||
* components. You could replace our default content with
|
||||
* custom SVG or CSS animations.
|
||||
*
|
||||
* @demo /docs/demos/src/refresher/
|
||||
*
|
||||
*/
|
||||
@Component({
|
||||
tag: 'ion-refresher',
|
||||
styleUrl: 'refresher.scss'
|
||||
})
|
||||
export class Refresher {
|
||||
|
||||
private appliedStyles = false;
|
||||
private didStart = false;
|
||||
private gestureConfig: any;
|
||||
private progress = 0;
|
||||
|
||||
scrollEl: HTMLElement;
|
||||
|
||||
@Prop({ context: 'dom' }) dom: DomController;
|
||||
|
||||
/**
|
||||
* 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 `complete()` it will begin the `completing` state.
|
||||
* - `completing` - 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() state: RefresherState = RefresherState.Inactive;
|
||||
|
||||
|
||||
@Element() el: HTMLElement;
|
||||
|
||||
/**
|
||||
* @input {number} The min distance the user must pull down until the
|
||||
* refresher can go into the `refreshing` state. Default is `60`.
|
||||
*/
|
||||
@Prop() pullMin = 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`.
|
||||
*/
|
||||
@Prop() pullDelta = 60;
|
||||
|
||||
// TODO: NEVER USED
|
||||
/**
|
||||
* @input {number} Time it takes to close the refresher. Default is `280ms`.
|
||||
*/
|
||||
@Prop() closeDuration = '280ms';
|
||||
|
||||
/**
|
||||
* @input {string} Time it takes the refresher to to snap back to the `refreshing` state. Default is `280ms`.
|
||||
*/
|
||||
@Prop() snapbackDuration = '280ms';
|
||||
|
||||
/**
|
||||
* @input {boolean} If the refresher is enabled or not. This should be used in place of an `ngIf`. Default is `true`.
|
||||
*/
|
||||
@Prop() enabled = false;
|
||||
|
||||
/**
|
||||
* @output {event} Emitted 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 `complete()` method when your async operation has completed.
|
||||
*/
|
||||
@Event() ionRefresh: EventEmitter;
|
||||
|
||||
/**
|
||||
* @output {event} Emitted while the user is pulling down the content and exposing the refresher.
|
||||
*/
|
||||
@Event() ionPull: EventEmitter;
|
||||
|
||||
/**
|
||||
* @output {event} Emitted when the user begins to start pulling down.
|
||||
*/
|
||||
@Event() ionStart: EventEmitter;
|
||||
|
||||
constructor() {
|
||||
this.gestureConfig = {
|
||||
'canStart': this.canStart.bind(this),
|
||||
'onStart': this.onStart.bind(this),
|
||||
'onMove': this.onMove.bind(this),
|
||||
'onEnd': this.onEnd.bind(this),
|
||||
'gestureName': 'refresher',
|
||||
'gesturePriority': 10,
|
||||
'type': 'pan',
|
||||
// 'passive': false,
|
||||
'direction': 'y',
|
||||
'threshold': 0,
|
||||
'attachTo': 'body'
|
||||
};
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
if (this.el.getAttribute('slot') !== 'fixed') {
|
||||
console.error('Make sure you use: <ion-refresher slot="fixed">');
|
||||
return;
|
||||
}
|
||||
this.scrollEl = this.el.parentElement.querySelector('ion-scroll') as HTMLElement;
|
||||
if (!this.scrollEl) {
|
||||
console.error('ion-refresher didn\'t attached, make sure if parent is a ion-content');
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
this.scrollEl = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call `complete()` 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 `completing`.
|
||||
*/
|
||||
@Method()
|
||||
complete() {
|
||||
this.close(RefresherState.Completing, '120ms');
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the refresher's state from `refreshing` to `cancelling`.
|
||||
*/
|
||||
@Method()
|
||||
cancel() {
|
||||
this.close(RefresherState.Cancelling, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Method()
|
||||
getProgress() {
|
||||
return this.progress;
|
||||
}
|
||||
|
||||
private canStart(): boolean {
|
||||
if (!this.scrollEl) {
|
||||
return false;
|
||||
}
|
||||
if (this.state !== RefresherState.Inactive) {
|
||||
return false;
|
||||
}
|
||||
// if the scrollTop is greater than zero then it's
|
||||
// not possible to pull the content down yet
|
||||
if (this.scrollEl.scrollTop > 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private onStart() {
|
||||
this.progress = 0;
|
||||
this.state = RefresherState.Inactive;
|
||||
}
|
||||
|
||||
private onMove(detail: GestureDetail) {
|
||||
// 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
|
||||
// if multitouch then get out immediately
|
||||
const ev = detail.event as TouchEvent;
|
||||
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.state & RefresherState._BUSY_) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
const deltaY = detail.deltaY;
|
||||
// don't bother if they're scrolling up
|
||||
// and have not already started dragging
|
||||
if (deltaY <= 0) {
|
||||
// the current Y is higher than the starting Y
|
||||
// so they scrolled up enough to be ignored
|
||||
this.progress = 0;
|
||||
this.state = RefresherState.Inactive;
|
||||
|
||||
if (this.appliedStyles) {
|
||||
// reset the styles only if they were applied
|
||||
this.setCss(0, '', false, '');
|
||||
return 5;
|
||||
}
|
||||
|
||||
return 6;
|
||||
}
|
||||
|
||||
if (this.state === RefresherState.Inactive) {
|
||||
// this refresh is not already actively pulling down
|
||||
// get the content's scrollTop
|
||||
const scrollHostScrollTop = this.scrollEl.scrollTop;
|
||||
|
||||
// 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 = RefresherState.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(deltaY, '0ms', true, '');
|
||||
|
||||
if (!deltaY) {
|
||||
// don't continue if there's no delta yet
|
||||
this.progress = 0;
|
||||
return 8;
|
||||
}
|
||||
|
||||
const pullMin = this.pullMin;
|
||||
// set pull progress
|
||||
this.progress = deltaY / pullMin;
|
||||
|
||||
// emit "start" if it hasn't started yet
|
||||
if (!this.didStart) {
|
||||
this.didStart = true;
|
||||
this.ionStart.emit(this);
|
||||
}
|
||||
|
||||
// emit "pulling" on every move
|
||||
this.ionPull.emit(this);
|
||||
|
||||
// do nothing if the delta is less than the pull threshold
|
||||
if (deltaY < pullMin) {
|
||||
// ensure it stays in the pulling state, cuz its not ready yet
|
||||
this.state = RefresherState.Pulling;
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (deltaY > pullMin + this.pullDelta) {
|
||||
// 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 = RefresherState.Ready;
|
||||
|
||||
return 4;
|
||||
}
|
||||
|
||||
private onEnd() {
|
||||
// only run in a zone when absolutely necessary
|
||||
if (this.state === RefresherState.Ready) {
|
||||
// they pulled down far enough, so it's ready to refresh
|
||||
this.beginRefresh();
|
||||
|
||||
} else if (this.state === RefresherState.Pulling) {
|
||||
// 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.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private beginRefresh() {
|
||||
// assumes we're already back in a zone
|
||||
// they pulled down far enough, so it's ready to refresh
|
||||
this.state = RefresherState.Refreshing;
|
||||
|
||||
// place the content in a hangout position while it thinks
|
||||
this.setCss(this.pullMin, this.snapbackDuration, true, '');
|
||||
|
||||
// emit "refresh" because it was pulled down far enough
|
||||
// and they let go to begin refreshing
|
||||
this.ionRefresh.emit(this);
|
||||
}
|
||||
|
||||
private close(state: RefresherState, delay: string) {
|
||||
let timer: number;
|
||||
|
||||
function close(ev: TransitionEvent) {
|
||||
// closing is done, return to inactive state
|
||||
if (ev) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
this.state = RefresherState.Inactive;
|
||||
this.progress = 0;
|
||||
this.didStart = false;
|
||||
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
|
||||
// TODO: what is this?
|
||||
// this.scrollEl.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);
|
||||
|
||||
// TODO: stop gesture
|
||||
// if (this._pointerEvents) {
|
||||
// this._pointerEvents.stop();
|
||||
// }
|
||||
}
|
||||
|
||||
private setCss(y: number, duration: string, overflowVisible: boolean, delay: string) {
|
||||
this.appliedStyles = (y > 0);
|
||||
this.dom.write(() => {
|
||||
const style = this.scrollEl.style;
|
||||
style.transform = ((y > 0) ? 'translateY(' + y + 'px) translateZ(0px)' : 'translateZ(0px)');
|
||||
style.transitionDuration = duration;
|
||||
style.transitionDelay = delay;
|
||||
style.overflow = (overflowVisible ? 'hidden' : '');
|
||||
});
|
||||
}
|
||||
|
||||
hostData() {
|
||||
return {
|
||||
class: {
|
||||
'refresher-active': this.state !== RefresherState.Inactive,
|
||||
'refresher-pulling': this.state === RefresherState.Pulling,
|
||||
'refresher-ready': this.state === RefresherState.Ready,
|
||||
'refresher-refreshing': this.state === RefresherState.Refreshing,
|
||||
'refresher-cancelling': this.state === RefresherState.Cancelling,
|
||||
'refresher-completing': this.state === RefresherState.Completing
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return <ion-gesture {...this.gestureConfig}
|
||||
enabled={this.enabled}>
|
||||
<slot></slot>
|
||||
</ion-gesture>;
|
||||
}
|
||||
}
|
||||
72
packages/core/src/components/refresher/test/basic.html
Normal file
72
packages/core/src/components/refresher/test/basic.html
Normal file
@@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html dir="ltr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Ionic Item Sliding</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<script src="/dist/ionic.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-page>
|
||||
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Ionic CDN demo</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-refresher id="refresher" enabled="true" slot="fixed">
|
||||
<ion-refresher-content></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<ion-list id="list"></ion-list>
|
||||
|
||||
</ion-content>
|
||||
|
||||
</ion-page>
|
||||
|
||||
</ion-app>
|
||||
<ion-menu-controller></ion-menu-controller>
|
||||
|
||||
<script>
|
||||
let items = [];
|
||||
for (var i = 0; i < 30; i++) {
|
||||
items.push( i+1 );
|
||||
}
|
||||
const list = document.getElementById('list');
|
||||
const refresher = document.getElementById('refresher');
|
||||
refresher.addEventListener('ionRefresh', async function() {
|
||||
console.log('Loading data...');
|
||||
const data = await getAsyncData();
|
||||
items = items.concat(data);
|
||||
refresher.complete();
|
||||
render();
|
||||
console.log('Done');
|
||||
});
|
||||
function render() {
|
||||
let html = '';
|
||||
for(let item of items) {
|
||||
html += `<ion-item>${item}</ion-item>`;
|
||||
}
|
||||
list.innerHTML = html;
|
||||
}
|
||||
function getAsyncData() {
|
||||
// async return mock data
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
let data = [];
|
||||
for (var i = 0; i < 30; i++) {
|
||||
data.push(i);
|
||||
}
|
||||
resolve(data);
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -37,6 +37,7 @@ exports.config = {
|
||||
{ components: ['ion-spinner'] },
|
||||
{ components: ['ion-split-pane'] },
|
||||
{ components: ['ion-range', 'ion-range-knob']},
|
||||
{ components: ['ion-refresher', 'ion-refresher-content']},
|
||||
{ components: ['ion-tabs', 'ion-tab', 'ion-tabbar', 'ion-tab-button'] },
|
||||
{ components: ['ion-toggle'] },
|
||||
{ components: ['ion-toast', 'ion-toast-controller'] },
|
||||
|
||||
Reference in New Issue
Block a user