mirror of
https://gitee.com/vant-contrib/vant.git
synced 2025-08-12 23:49:46 +08:00
236 lines
5.5 KiB
TypeScript
236 lines
5.5 KiB
TypeScript
import { ref, watch, reactive, nextTick, defineComponent } from 'vue';
|
|
|
|
// Utils
|
|
import { preventDefault, getScrollTop, createNamespace } from '../utils';
|
|
|
|
// Composables
|
|
import { useScrollParent } from '@vant/use';
|
|
import { useTouch } from '../composables/use-touch';
|
|
|
|
// Components
|
|
import { Loading } from '../loading';
|
|
|
|
const [name, bem, t] = createNamespace('pull-refresh');
|
|
|
|
const DEFAULT_HEAD_HEIGHT = 50;
|
|
const TEXT_STATUS = ['pulling', 'loosing', 'success'];
|
|
|
|
type PullRefreshStatus =
|
|
| 'normal'
|
|
| 'loading'
|
|
| 'loosing'
|
|
| 'pulling'
|
|
| 'success';
|
|
|
|
export default defineComponent({
|
|
name,
|
|
|
|
props: {
|
|
disabled: Boolean,
|
|
successText: String,
|
|
pullingText: String,
|
|
loosingText: String,
|
|
loadingText: String,
|
|
pullDistance: [Number, String],
|
|
modelValue: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
successDuration: {
|
|
type: [Number, String],
|
|
default: 500,
|
|
},
|
|
animationDuration: {
|
|
type: [Number, String],
|
|
default: 300,
|
|
},
|
|
headHeight: {
|
|
type: [Number, String],
|
|
default: DEFAULT_HEAD_HEIGHT,
|
|
},
|
|
},
|
|
|
|
emits: ['refresh', 'update:modelValue'],
|
|
|
|
setup(props, { emit, slots }) {
|
|
let reachTop: boolean;
|
|
|
|
const root = ref<HTMLElement>();
|
|
const scrollParent = useScrollParent(root);
|
|
|
|
const state = reactive({
|
|
status: 'normal' as PullRefreshStatus,
|
|
distance: 0,
|
|
duration: 0,
|
|
});
|
|
|
|
const touch = useTouch();
|
|
|
|
const getHeadStyle = () => {
|
|
if (props.headHeight !== DEFAULT_HEAD_HEIGHT) {
|
|
return {
|
|
height: `${props.headHeight}px`,
|
|
};
|
|
}
|
|
};
|
|
|
|
const isTouchable = () =>
|
|
state.status !== 'loading' &&
|
|
state.status !== 'success' &&
|
|
!props.disabled;
|
|
|
|
const ease = (distance: number) => {
|
|
const pullDistance = +(props.pullDistance || props.headHeight);
|
|
|
|
if (distance > pullDistance) {
|
|
if (distance < pullDistance * 2) {
|
|
distance = pullDistance + (distance - pullDistance) / 2;
|
|
} else {
|
|
distance = pullDistance * 1.5 + (distance - pullDistance * 2) / 4;
|
|
}
|
|
}
|
|
|
|
return Math.round(distance);
|
|
};
|
|
|
|
const setStatus = (distance: number, isLoading?: boolean) => {
|
|
const pullDistance = +(props.pullDistance || props.headHeight);
|
|
state.distance = distance;
|
|
|
|
if (isLoading) {
|
|
state.status = 'loading';
|
|
} else if (distance === 0) {
|
|
state.status = 'normal';
|
|
} else if (distance < pullDistance) {
|
|
state.status = 'pulling';
|
|
} else {
|
|
state.status = 'loosing';
|
|
}
|
|
};
|
|
|
|
const getStatusText = () => {
|
|
const { status } = state;
|
|
if (status === 'normal') {
|
|
return '';
|
|
}
|
|
return props[`${status}Text` as const] || t(status);
|
|
};
|
|
|
|
const renderStatus = () => {
|
|
const { status, distance } = state;
|
|
|
|
if (slots[status]) {
|
|
return slots[status]!({ distance });
|
|
}
|
|
|
|
const nodes = [];
|
|
|
|
if (TEXT_STATUS.includes(status)) {
|
|
nodes.push(<div class={bem('text')}>{getStatusText()}</div>);
|
|
}
|
|
if (status === 'loading') {
|
|
nodes.push(<Loading class={bem('loading')}>{getStatusText()}</Loading>);
|
|
}
|
|
|
|
return nodes;
|
|
};
|
|
|
|
const showSuccessTip = () => {
|
|
state.status = 'success';
|
|
|
|
setTimeout(() => {
|
|
setStatus(0);
|
|
}, +props.successDuration);
|
|
};
|
|
|
|
const checkPosition = (event: TouchEvent) => {
|
|
reachTop = getScrollTop(scrollParent.value!) === 0;
|
|
|
|
if (reachTop) {
|
|
state.duration = 0;
|
|
touch.start(event);
|
|
}
|
|
};
|
|
|
|
const onTouchStart = (event: TouchEvent) => {
|
|
if (isTouchable()) {
|
|
checkPosition(event);
|
|
}
|
|
};
|
|
|
|
const onTouchMove = (event: TouchEvent) => {
|
|
if (isTouchable()) {
|
|
if (!reachTop) {
|
|
checkPosition(event);
|
|
}
|
|
|
|
const { deltaY } = touch;
|
|
touch.move(event);
|
|
|
|
if (reachTop && deltaY.value >= 0 && touch.isVertical()) {
|
|
preventDefault(event);
|
|
setStatus(ease(deltaY.value));
|
|
}
|
|
}
|
|
};
|
|
|
|
const onTouchEnd = () => {
|
|
if (reachTop && touch.deltaY.value && isTouchable()) {
|
|
state.duration = +props.animationDuration;
|
|
|
|
if (state.status === 'loosing') {
|
|
setStatus(+props.headHeight, true);
|
|
emit('update:modelValue', true);
|
|
|
|
// ensure value change can be watched
|
|
nextTick(() => emit('refresh'));
|
|
} else {
|
|
setStatus(0);
|
|
}
|
|
}
|
|
};
|
|
|
|
watch(
|
|
() => props.modelValue,
|
|
(value) => {
|
|
state.duration = +props.animationDuration;
|
|
|
|
if (value) {
|
|
setStatus(+props.headHeight, true);
|
|
} else if (slots.success || props.successText) {
|
|
showSuccessTip();
|
|
} else {
|
|
setStatus(0, false);
|
|
}
|
|
}
|
|
);
|
|
|
|
return () => {
|
|
const trackStyle = {
|
|
transitionDuration: `${state.duration}ms`,
|
|
transform: state.distance
|
|
? `translate3d(0,${state.distance}px, 0)`
|
|
: '',
|
|
};
|
|
|
|
return (
|
|
<div ref={root} class={bem()}>
|
|
<div
|
|
class={bem('track')}
|
|
style={trackStyle}
|
|
onTouchstart={onTouchStart}
|
|
onTouchmove={onTouchMove}
|
|
onTouchend={onTouchEnd}
|
|
onTouchcancel={onTouchEnd}
|
|
>
|
|
<div class={bem('head')} style={getHeadStyle()}>
|
|
{renderStatus()}
|
|
</div>
|
|
{slots.default?.()}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
},
|
|
});
|