mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-10 00:27:41 +08:00
@ -1,7 +1,11 @@
|
||||
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, State, Watch, h, writeTask } from '@stencil/core';
|
||||
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, State, Watch, h, readTask, writeTask } from '@stencil/core';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import { Gesture, GestureDetail, RefresherEventDetail } from '../../interface';
|
||||
import { clamp } from '../../utils/helpers';
|
||||
import { hapticImpact } from '../../utils/native/haptic';
|
||||
|
||||
import { handleScrollWhilePulling, handleScrollWhileRefreshing, setSpinnerOpacity, shouldUseNativeRefresher, translateElement } from './refresher.utils';
|
||||
|
||||
@Component({
|
||||
tag: 'ion-refresher',
|
||||
@ -16,9 +20,18 @@ export class Refresher implements ComponentInterface {
|
||||
private didStart = false;
|
||||
private progress = 0;
|
||||
private scrollEl?: HTMLElement;
|
||||
private scrollListenerCallback?: any;
|
||||
private gesture?: Gesture;
|
||||
|
||||
@Element() el!: HTMLElement;
|
||||
private pointerDown = false;
|
||||
private needsCompletion = false;
|
||||
private didRefresh = false;
|
||||
private lastVelocityY = 0;
|
||||
private elementToTransform?: HTMLElement;
|
||||
|
||||
@State() private nativeRefresher = false;
|
||||
|
||||
@Element() el!: HTMLIonRefresherElement;
|
||||
|
||||
/**
|
||||
* The current state which the refresher is in. The refresher's states include:
|
||||
@ -35,6 +48,8 @@ export class Refresher implements ComponentInterface {
|
||||
/**
|
||||
* The minimum distance the user must pull down until the
|
||||
* refresher will go into the `refreshing` state.
|
||||
* Does not apply when the refresher content uses a spinner,
|
||||
* enabling the native refresher.
|
||||
*/
|
||||
@Prop() pullMin = 60;
|
||||
|
||||
@ -42,16 +57,22 @@ export class Refresher implements ComponentInterface {
|
||||
* The maximum distance of the pull until the refresher
|
||||
* will automatically go into the `refreshing` state.
|
||||
* Defaults to the result of `pullMin + 60`.
|
||||
* Does not apply when the refresher content uses a spinner,
|
||||
* enabling the native refresher.
|
||||
*/
|
||||
@Prop() pullMax: number = this.pullMin + 60;
|
||||
|
||||
/**
|
||||
* Time it takes to close the refresher.
|
||||
* Does not apply when the refresher content uses a spinner,
|
||||
* enabling the native refresher.
|
||||
*/
|
||||
@Prop() closeDuration = '280ms';
|
||||
|
||||
/**
|
||||
* Time it takes the refresher to to snap back to the `refreshing` state.
|
||||
* Does not apply when the refresher content uses a spinner,
|
||||
* enabling the native refresher.
|
||||
*/
|
||||
@Prop() snapbackDuration = '280ms';
|
||||
|
||||
@ -65,6 +86,9 @@ export class Refresher implements ComponentInterface {
|
||||
* `10` pixels, instead of `10` pixels the content will be pulled by `12` pixels
|
||||
* (an increase of 20 percent). If the value passed is `0.8`, the dragged amount
|
||||
* will be `8` pixels, less than the amount the cursor has moved.
|
||||
*
|
||||
* Does not apply when the refresher content uses a spinner,
|
||||
* enabling the native refresher.
|
||||
*/
|
||||
@Prop() pullFactor = 1;
|
||||
|
||||
@ -97,34 +121,192 @@ export class Refresher implements ComponentInterface {
|
||||
*/
|
||||
@Event() ionStart!: EventEmitter<void>;
|
||||
|
||||
private checkNativeRefresher() {
|
||||
if (shouldUseNativeRefresher(this.el, getIonMode(this))) {
|
||||
const contentEl = this.el.closest('ion-content');
|
||||
this.setupNativeRefresher(contentEl);
|
||||
} else {
|
||||
this.destroyNativeRefresher();
|
||||
}
|
||||
}
|
||||
|
||||
private destroyNativeRefresher() {
|
||||
if (this.scrollEl && this.scrollListenerCallback) {
|
||||
this.scrollEl.removeEventListener('scroll', this.scrollListenerCallback);
|
||||
this.scrollListenerCallback = undefined;
|
||||
}
|
||||
|
||||
this.nativeRefresher = false;
|
||||
}
|
||||
|
||||
private async resetNativeRefresher(el: HTMLElement | undefined, state: RefresherState) {
|
||||
this.state = state;
|
||||
|
||||
if (el !== undefined) {
|
||||
await translateElement(el, undefined);
|
||||
}
|
||||
|
||||
this.didRefresh = false;
|
||||
this.needsCompletion = false;
|
||||
this.pointerDown = false;
|
||||
this.state = RefresherState.Inactive;
|
||||
}
|
||||
|
||||
private async setupNativeRefresher(contentEl: HTMLIonContentElement | null) {
|
||||
if (this.scrollListenerCallback || !contentEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pullingSpinner = this.el.querySelector('ion-refresher-content .refresher-pulling ion-spinner') as HTMLElement;
|
||||
const refreshingSpinner = this.el.querySelector('ion-refresher-content .refresher-refreshing ion-spinner') as HTMLElement;
|
||||
|
||||
this.elementToTransform = this.scrollEl!.querySelector(`#scroll-content`) as HTMLElement | undefined;
|
||||
this.nativeRefresher = true;
|
||||
const ticks = pullingSpinner.shadowRoot!.querySelectorAll('svg');
|
||||
const MAX_PULL = this.scrollEl!.clientHeight * 0.16;
|
||||
const NUM_TICKS = ticks.length;
|
||||
|
||||
writeTask(() => ticks.forEach(el => el.style.setProperty('animation', 'none')));
|
||||
|
||||
this.scrollListenerCallback = () => {
|
||||
// If pointer is not on screen or refresher is not active, ignore scroll
|
||||
if (!this.pointerDown && this.state === RefresherState.Inactive) { return; }
|
||||
|
||||
readTask(() => {
|
||||
// PTR should only be active when overflow scrolling at the top
|
||||
const scrollTop = this.scrollEl!.scrollTop;
|
||||
const refresherHeight = this.el.clientHeight;
|
||||
|
||||
if (scrollTop > 0) {
|
||||
/**
|
||||
* If refresher is refreshing and user tries to scroll
|
||||
* progressively fade refresher out/in
|
||||
*/
|
||||
if (this.state === RefresherState.Refreshing) {
|
||||
const ratio = clamp(0, scrollTop / (refresherHeight * 0.5), 1);
|
||||
writeTask(() => setSpinnerOpacity(refreshingSpinner, 1 - ratio));
|
||||
return;
|
||||
}
|
||||
|
||||
writeTask(() => setSpinnerOpacity(pullingSpinner, 0));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.pointerDown) {
|
||||
if (!this.didStart) {
|
||||
this.didStart = true;
|
||||
this.ionStart.emit();
|
||||
}
|
||||
|
||||
this.ionPull.emit();
|
||||
}
|
||||
|
||||
// delay showing the next tick marks until user has pulled 30px
|
||||
const opacity = clamp(0, Math.abs(scrollTop) / refresherHeight, 0.99);
|
||||
const pullAmount = this.progress = clamp(0, (Math.abs(scrollTop) - 30) / MAX_PULL, 1);
|
||||
const currentTickToShow = clamp(0, Math.floor(pullAmount * NUM_TICKS), NUM_TICKS - 1);
|
||||
const shouldShowRefreshingSpinner = this.state === RefresherState.Refreshing || currentTickToShow === NUM_TICKS - 1;
|
||||
|
||||
if (shouldShowRefreshingSpinner) {
|
||||
if (this.pointerDown) {
|
||||
handleScrollWhileRefreshing(refreshingSpinner, this.lastVelocityY);
|
||||
}
|
||||
|
||||
if (!this.didRefresh) {
|
||||
this.beginRefresh();
|
||||
this.didRefresh = true;
|
||||
hapticImpact({ style: 'light' });
|
||||
|
||||
/**
|
||||
* Translate the content element otherwise when pointer is removed
|
||||
* from screen the scroll content will bounce back over the refresher
|
||||
*/
|
||||
if (!this.pointerDown) {
|
||||
translateElement(this.elementToTransform, `${refresherHeight}px`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.state = RefresherState.Pulling;
|
||||
handleScrollWhilePulling(pullingSpinner, ticks, opacity, currentTickToShow);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.scrollEl!.addEventListener('scroll', this.scrollListenerCallback);
|
||||
|
||||
this.gesture = (await import('../../utils/gesture')).createGesture({
|
||||
el: this.scrollEl!,
|
||||
gestureName: 'refresher',
|
||||
gesturePriority: 10,
|
||||
direction: 'y',
|
||||
threshold: 0,
|
||||
onStart: () => {
|
||||
this.pointerDown = true;
|
||||
|
||||
if (!this.didRefresh) {
|
||||
translateElement(this.elementToTransform, '0px');
|
||||
}
|
||||
},
|
||||
onMove: ev => {
|
||||
this.lastVelocityY = ev.velocityY;
|
||||
},
|
||||
onEnd: () => {
|
||||
this.pointerDown = false;
|
||||
this.didStart = false;
|
||||
|
||||
if (this.needsCompletion) {
|
||||
this.resetNativeRefresher(this.elementToTransform, RefresherState.Completing);
|
||||
this.needsCompletion = false;
|
||||
} else if (this.didRefresh) {
|
||||
readTask(() => {
|
||||
translateElement(this.elementToTransform, `${this.el.clientHeight}px`);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this.disabledChanged();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.checkNativeRefresher();
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
if (this.el.getAttribute('slot') !== 'fixed') {
|
||||
console.error('Make sure you use: <ion-refresher slot="fixed">');
|
||||
return;
|
||||
}
|
||||
|
||||
const contentEl = this.el.closest('ion-content');
|
||||
if (!contentEl) {
|
||||
console.error('<ion-refresher> must be used inside an <ion-content>');
|
||||
return;
|
||||
}
|
||||
this.scrollEl = await contentEl.getScrollElement();
|
||||
this.gesture = (await import('../../utils/gesture')).createGesture({
|
||||
el: contentEl,
|
||||
gestureName: 'refresher',
|
||||
gesturePriority: 10,
|
||||
direction: 'y',
|
||||
threshold: 20,
|
||||
passive: false,
|
||||
canStart: () => this.canStart(),
|
||||
onStart: () => this.onStart(),
|
||||
onMove: ev => this.onMove(ev),
|
||||
onEnd: () => this.onEnd(),
|
||||
});
|
||||
|
||||
this.disabledChanged();
|
||||
this.scrollEl = await contentEl.getScrollElement();
|
||||
if (shouldUseNativeRefresher(this.el, getIonMode(this))) {
|
||||
this.setupNativeRefresher(contentEl);
|
||||
} else {
|
||||
this.gesture = (await import('../../utils/gesture')).createGesture({
|
||||
el: contentEl,
|
||||
gestureName: 'refresher',
|
||||
gesturePriority: 10,
|
||||
direction: 'y',
|
||||
threshold: 20,
|
||||
passive: false,
|
||||
canStart: () => this.canStart(),
|
||||
onStart: () => this.onStart(),
|
||||
onMove: ev => this.onMove(ev),
|
||||
onEnd: () => this.onEnd(),
|
||||
});
|
||||
|
||||
this.disabledChanged();
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.destroyNativeRefresher();
|
||||
this.scrollEl = undefined;
|
||||
if (this.gesture) {
|
||||
this.gesture.destroy();
|
||||
@ -143,7 +325,16 @@ export class Refresher implements ComponentInterface {
|
||||
*/
|
||||
@Method()
|
||||
async complete() {
|
||||
this.close(RefresherState.Completing, '120ms');
|
||||
if (this.nativeRefresher) {
|
||||
this.needsCompletion = true;
|
||||
|
||||
// Do not reset scroll el until user removes pointer from screen
|
||||
if (!this.pointerDown) {
|
||||
this.resetNativeRefresher(this.elementToTransform, RefresherState.Completing);
|
||||
}
|
||||
} else {
|
||||
this.close(RefresherState.Completing, '120ms');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -151,7 +342,14 @@ export class Refresher implements ComponentInterface {
|
||||
*/
|
||||
@Method()
|
||||
async cancel() {
|
||||
this.close(RefresherState.Cancelling, '');
|
||||
if (this.nativeRefresher) {
|
||||
// Do not reset scroll el until user removes pointer from screen
|
||||
if (!this.pointerDown) {
|
||||
this.resetNativeRefresher(this.elementToTransform, RefresherState.Cancelling);
|
||||
}
|
||||
} else {
|
||||
this.close(RefresherState.Cancelling, '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -341,6 +539,8 @@ export class Refresher implements ComponentInterface {
|
||||
}
|
||||
|
||||
private setCss(y: number, duration: string, overflowVisible: boolean, delay: string) {
|
||||
if (this.nativeRefresher) { return; }
|
||||
|
||||
this.appliedStyles = (y > 0);
|
||||
writeTask(() => {
|
||||
if (this.scrollEl) {
|
||||
@ -363,13 +563,13 @@ export class Refresher implements ComponentInterface {
|
||||
|
||||
// Used internally for styling
|
||||
[`refresher-${mode}`]: true,
|
||||
|
||||
'refresher-native': this.nativeRefresher,
|
||||
'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
|
||||
'refresher-completing': this.state === RefresherState.Completing,
|
||||
}}
|
||||
>
|
||||
</Host>
|
||||
|
||||
Reference in New Issue
Block a user