diff --git a/README.md b/README.md index 2bbaea6..31f3d8d 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,24 @@ # react-pullrefresh -Pull to reflesh material design component. +Pull to reflesh material design component.
react-native is supported. ![](/2017_03_06_13_09_14.gif?raw=true) -#### Demo +## Demo -[https://yusukeshibata.github.io/react-pullrefresh/](https://yusukeshibata.github.io/react-pullrefresh/) + +## Install -#### Install - - ```sh - npm install react-pullrefresh - ``` +```sh +npm install react-pullrefresh +``` -#### Usage +## Usage - ```javascript - import PullRefresh from 'react-pullrefresh' +```javascript +import PullRefresh from 'react-pullrefresh' class App extends Component { // onRefresh function canbe async/sync @@ -44,22 +43,106 @@ react-native is supported. export default App ``` + +### HOC (High Order Component) + +```javascript +import PullRefresh, { Indicator } from 'react-pullrefresh' + +const PortalIncidator = PortalHoc(Indicator) +// control props namespace for List component. +// add extra onRequestMore and pullFreshProps prop that not conflict with List Component. +function PullRefreshHoc(AnotherComponent) { + return class _PullRefreshHoc extends React.Component { + handleRefresh = () => { + if (typeof this.props.onRequestMore === 'function') { + const reason = "pullRefresh" + return this.props.onRequestMore(reason) + } + } + + render() { + // use pullFreshProps props namespace. + const { pullFreshProps, onRequestMore, ...otherProps } = this.props; + const defautWraperComponent = React.Fragment + const _pullFreshProps = Object.assign( + // change default wraperComponent to React.Fragment. + { + wraperComponent: defautWraperComponent + IndicatorComponent: PortalIncidator + }, + pullFreshProps, + // pullFreshProps should never override AnotherComponent. + { + component: AnotherComponent, + onRefresh: this.handleRefresh + }) + return ( + + ) + } + } +} + + +// EnhancedList get extra two prop(onRequestMore, pullFreshProps) +// for pull refresh feature. +export const EnhancedList = PullRefreshHoc(List) + +// or more enhance +const enhance = compose(FlipMoveHoc, InfiniteLoadHoc, ...OtherFeatureHocs) +export const MulitiEnhancedList = enhance(EnhancedList) + + +const handleRequestMore = async (reason) => { + if (reason === 'pullRefresh') { + await fetchData({page: 1}) + } else if (reason === 'bottomInfiniteLoad') { + await fetchData({page: getNextPage()}) + } +} + +// List's prop disabled and component not conflict with pullRefresh props. +const pullRefreshProps = { + color: "#ff0000", + disabled: false, + zIndex: 20 +} +const list = ( + + {listItems} + +) +``` + #### Behaviour difference between v1/v2 TODO: #### Props -##### render +##### render TODO: - -##### color +##### color default: `#787878` -##### bgColor +##### bgColor default: `#ffffff` @@ -91,13 +174,13 @@ default: `undefined` #### Removed props -* size -* offset -* max -* waitingComponent -* pullingComponent -* pulledComponent -* supportDesktop +- size +- offset +- max +- waitingComponent +- pullingComponent +- pulledComponent +- supportDesktop #### License diff --git a/src/component/index.js b/src/component/index.js index 9b22c0a..65b48bd 100644 --- a/src/component/index.js +++ b/src/component/index.js @@ -30,7 +30,8 @@ const Component = styled.div` border-radius: 20px; width: 40px; height: 40px; - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), + 0 3px 1px -2px rgba(0, 0, 0, 0.2); ` const RotatingSvg = styled.svg` @@ -44,9 +45,8 @@ const DashedCircle = styled.circle` animation: ${dashed} 1.4s ease-in-out infinite; ` -export default (props, state) => { - const { max, yRefreshing, y, phase } = state - const { zIndex, color, bgColor } = props +export default props => { + const { zIndex, color, bgColor, max, yRefreshing, y, phase } = props const p = Math.atan(y / max) const pMax = Math.atan(yRefreshing / max) const r = Math.PI * 10 * 2 @@ -55,47 +55,47 @@ export default (props, state) => { const refreshed = phase === 'refreshed' return ( - { phase !== 'refreshing' && - - } + {phase !== 'refreshing' && ( + + )} ) } - - diff --git a/src/component/index.native.js b/src/component/index.native.js index 650c9d2..871c153 100644 --- a/src/component/index.native.js +++ b/src/component/index.native.js @@ -1,7 +1,11 @@ import React, { Component } from 'react' import styled from 'styled-components/native' import { Easing, Animated, View } from 'react-native' -import { Svg as NativeSvg, Circle as NativeCircle, Path as NativePath } from 'react-native-svg' +import { + Svg as NativeSvg, + Circle as NativeCircle, + Path as NativePath +} from 'react-native-svg' class RotatingSvg extends Component { constructor(props) { @@ -12,14 +16,16 @@ class RotatingSvg extends Component { } } componentDidMount() { - this.state.r.addListener((r) => { + this.state.r.addListener(r => { this.setState({ value: r.value }) }) - this._animated = Animated.loop(Animated.timing(this.state.r, { - easing: Easing.linear, - toValue: 270, - duration: 1400, - })) + this._animated = Animated.loop( + Animated.timing(this.state.r, { + easing: Easing.linear, + toValue: 270, + duration: 1400 + }) + ) this._animated.start() } componentWillUnmount() { @@ -31,11 +37,8 @@ class RotatingSvg extends Component { return ( @@ -48,7 +51,7 @@ class DashedCircle extends Component { super(props) this.state = { rotate: new Animated.Value(0), - dash: new Animated.Value(62), + dash: new Animated.Value(62) } } componentDidMount() { @@ -58,28 +61,30 @@ class DashedCircle extends Component { this.state.dash.addListener(d => { this.setState({ d: d.value }) }) - this._animated = Animated.loop(Animated.parallel([ - Animated.sequence([ - Animated.timing(this.state.rotate, { - toValue: 135, - duration: 700, - }), - Animated.timing(this.state.rotate, { - toValue: 450, - duration: 700 - }), - ]), - Animated.sequence([ - Animated.timing(this.state.dash, { - toValue: 62/4, - duration: 700 - }), - Animated.timing(this.state.dash, { - toValue: 62, - duration: 700 - }), + this._animated = Animated.loop( + Animated.parallel([ + Animated.sequence([ + Animated.timing(this.state.rotate, { + toValue: 135, + duration: 700 + }), + Animated.timing(this.state.rotate, { + toValue: 450, + duration: 700 + }) + ]), + Animated.sequence([ + Animated.timing(this.state.dash, { + toValue: 62 / 4, + duration: 700 + }), + Animated.timing(this.state.dash, { + toValue: 62, + duration: 700 + }) + ]) ]) - ])) + ) this._animated.start() } componentWillUnmount() { @@ -90,15 +95,12 @@ class DashedCircle extends Component { const { strokeDasharray, style, ...props } = this.props return ( ) @@ -117,9 +119,8 @@ const Container = styled(View)` shadow-color: #000; ` -export default (props, state, children) => { - const { max, yRefreshing, y, phase } = state - const { color, bgColor } = props +export default (props, children) => { + const { color, bgColor, max, yRefreshing, y, phase } = props const p = Math.atan(y / max) const pMax = Math.atan(yRefreshing / max) const r = Math.PI * 10 * 2 @@ -129,7 +130,7 @@ export default (props, state, children) => { return [ children, { - { phase !== 'refreshing' && - - } + {phase !== 'refreshing' && ( + + )} ] } - - diff --git a/src/index.js b/src/index.js index cf0c3c6..798d345 100644 --- a/src/index.js +++ b/src/index.js @@ -1,12 +1,13 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' + import Spring from './spring' -import renderDefault from './component' +import Indicator from './component' const MAX = 100 const sleep = msec => new Promise(resolve => setTimeout(resolve, msec)) -export default class PullRefresh extends Component { +class PullRefresh extends Component { constructor(props) { super(props) this.state = { @@ -15,6 +16,14 @@ export default class PullRefresh extends Component { max: MAX, phase: '' } + this.onScroll = this.onScroll.bind(this) + this.onMouseDown = this.onMouseDown.bind(this) + this.onMouseUp = this.onMouseUp.bind(this) + this.onMouseMove = this.onMouseMove.bind(this) + this.onTouchStart = this.onTouchStart.bind(this) + this.onTouchEnd = this.onTouchEnd.bind(this) + this.onTouchMove = this.onTouchMove.bind(this) + this.onSpringUpdate = this.onSpringUpdate.bind(this) } async refresh() { this.setState({ @@ -26,7 +35,7 @@ export default class PullRefresh extends Component { async _refresh() { const { max, phase } = this.state const { onRefresh } = this.props - if(phase === 'willRefresh') { + if (phase === 'willRefresh') { this._willRefresh = true await this._spring.to(max) this._spring.pause() @@ -45,33 +54,104 @@ export default class PullRefresh extends Component { this._spring.endValue = this._y } onScroll(evt) { - this._scrollTop = evt.currentTarget.scrollTop !== undefined - ? evt.currentTarget.scrollTop : evt.nativeEvent.contentOffset.y + if (this.props.onScroll) { + this.props.onScroll(evt) + } + if (!this.props.disabled) { + this._scrollTop = + evt.currentTarget.scrollTop !== undefined + ? evt.currentTarget.scrollTop + : evt.nativeEvent.contentOffset.y + } + } + onMouseDown(evt) { + if (this.props.onMouseDown) { + this.props.onMouseDown(evt) + } + if (!this.props.disabled && !this.props.disableMouse) { + this.onDown(evt) + } + } + onTouchStart(evt) { + if (this.props.onTouchStart) { + this.props.onTouchStart(evt) + } + if (!this.props.disabled && !this.props.disableTouch) { + this.onDown(evt) + } } onDown(evt) { const { phase } = this.state - if(this._willRefresh) return - if(phase === 'refreshed' || phase === 'refreshing') return + if (this._willRefresh) return + if (phase === 'refreshed' || phase === 'refreshing') return this._down = true - const ey = evt.nativeEvent.touches ? evt.nativeEvent.touches[0].pageY : evt.pageY + const ey = evt.nativeEvent.touches + ? evt.nativeEvent.touches[0].pageY + : evt.pageY + const ex = evt.nativeEvent.touches + ? evt.nativeEvent.touches[0].pageX + : evt.pageX this._py = ey + this._px = ex + } + async onMouseUp(evt) { + if (this.props.onMouseUp) { + this.props.onMouseUp(evt) + } + if (!this.props.disabled && !this.props.disableMouse) { + await this.onUp(evt) + } + } + async onTouchEnd(evt) { + if (this.props.onTouchEnd) { + this.props.onTouchEnd(evt) + } + if (!this.props.disabled && !this.props.disableTouch) { + await this.onUp(evt) + } } async onUp(evt) { const { phase } = this.state - if(phase === 'refreshed' || phase === 'refreshing') return + if (phase === 'refreshed' || phase === 'refreshing') return this._down = false await this._refresh() } + onMouseMove(evt) { + if (this.props.onMouseMove) { + this.props.onMouseMove(evt) + } + if (!this.props.disabled && !this.props.disableMouse) { + this.onMove(evt) + } + } + onTouchMove(evt) { + if (this.props.onTouchMove) { + this.props.onTouchMove(evt) + } + if (!this.props.disabled && !this.props.disableTouch) { + this.onMove(evt) + } + } onMove(evt) { const { phase } = this.state - if(this._willRefresh || !this._down) return - if(phase === 'refreshed' || phase === 'refreshing') return - const ey = evt.nativeEvent.touches ? evt.nativeEvent.touches[0].pageY : evt.pageY - if(this._scrollTop <= 0) { - this._y = this._y + ey - this._py - this._spring.endValue = this._y + if (this._willRefresh || !this._down) return + if (phase === 'refreshed' || phase === 'refreshing') return + const ey = evt.nativeEvent.touches + ? evt.nativeEvent.touches[0].pageY + : evt.pageY + const ex = evt.nativeEvent.touches + ? evt.nativeEvent.touches[0].pageX + : evt.pageX + const vy = ey - this._py + const vx = ex - this._px + if (this._scrollTop <= 0 && Math.abs(vy) > Math.abs(vx)) { + if (vy >= 10 || vy < 0) { + this._y = this._y + vy + this._spring.endValue = this._y + } } this._py = ey + this._px = ex } onSpringUpdate(spring) { const { max, yRefreshing, phase } = this.state @@ -80,11 +160,11 @@ export default class PullRefresh extends Component { y, yRefreshing: this._willRefresh ? Math.max(y, yRefreshing) : y }) - if(phase !== 'refreshed' && phase !== 'refreshing') { - const newPhase = y >= max ? 'willRefresh' : '' - if(phase !== newPhase) this.setState({ phase: newPhase }) + if (phase !== 'refreshed' && phase !== 'refreshing') { + const newPhase = y >= max ? 'willRefresh' : '' + if (phase !== newPhase) this.setState({ phase: newPhase }) } - if(phase === 'refreshed' && y === 0) { + if (phase === 'refreshed' && y === 0) { this.setState({ phase: '' }) } } @@ -92,60 +172,173 @@ export default class PullRefresh extends Component { this._y = 0 this._scrollTop = 0 this._spring = new Spring(60, 10) - this._spring.onUpdate = ::this.onSpringUpdate + this._spring.onUpdate = this.onSpringUpdate } render() { - const { + const { zIndex, color, bgColor } = this.props + const { max, yRefreshing, y, phase } = this.state + const indicatorProps = { zIndex, - render, - bgColor, color, - onRefresh, - disabled, - as, - children, - ...props - } = this.props - const PullRefreshComponent = render - const Container = as - return ( -
- - { children } - - { render(this.props, this.state) } -
- ) + bgColor, + max, + yRefreshing, + y, + phase + } + const { IndicatorComponent } = this.props + return } } PullRefresh.propTypes = { - as: PropTypes.oneOfType([ PropTypes.object, PropTypes.string ]), + wraperComponent: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.object, + PropTypes.string + ]), + component: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), onRefresh: PropTypes.func, style: PropTypes.object, disabled: PropTypes.bool, + disableMouse: PropTypes.bool, + disableTouch: PropTypes.bool, color: PropTypes.string, bgColor: PropTypes.string, - render: PropTypes.func, + IndicatorComponent: PropTypes.func, zIndex: PropTypes.number } PullRefresh.defaultProps = { - as: 'div', style: {}, disabled: false, + disableMouse: false, + disableTouch: false, color: '#4285f4', bgColor: '#fff', - render: renderDefault, + IndicatorComponent: Indicator, zIndex: undefined } + +class PullRefreshConvertProps extends Component { + constructor(props) { + super(props) + this.setPullRefreshRef = this.setPullRefreshRef.bind(this) + this.onScroll = this.onScroll.bind(this) + this.onMouseDown = this.onMouseDown.bind(this) + this.onMouseUp = this.onMouseUp.bind(this) + this.onMouseMove = this.onMouseMove.bind(this) + this.onTouchStart = this.onTouchStart.bind(this) + this.onTouchEnd = this.onTouchEnd.bind(this) + this.onTouchMove = this.onTouchMove.bind(this) + } + setPullRefreshRef(pr) { + this.pr = pr + } + onScroll(e) { + this.pr.onScroll(e) + } + onMouseDown(e) { + this.pr.onMouseDown(e) + } + onMouseUp(e) { + this.pr.onMouseUp(e) + } + onMouseMove(e) { + this.pr.onMouseMove(e) + } + onTouchStart(e) { + this.pr.onTouchStart(e) + } + onTouchEnd(e) { + this.pr.onTouchEnd(e) + } + onTouchMove(e) { + this.pr.onTouchMove(e) + } + render() { + const { + onScroll, + onMouseDown, + onMouseUp, + onMouseMove, + onTouchStart, + onTouchEnd, + onTouchMove, + pullRefreshProps, + ...componentProps + } = this.props + const { + zIndex, + render, + bgColor, + color, + onRefresh, + disabled, + disableMouse, + disableTouch + } = this.props + const _pullRefreshProps = + pullRefreshProps !== null && typeof pullRefreshProps === 'object' + ? pullRefreshProps + : { + zIndex, + render, + bgColor, + color, + onRefresh, + disabled, + disableMouse, + disableTouch + } + const { wraperComponent, component } = _pullRefreshProps + const Component = component || 'div' + let Wraper + if (wraperComponent === null) { + Wraper = React.Fragment + } else if (!wraperComponent) { + Wraper = 'div' + } else { + Wraper = wraperComponent + } + return ( + + + + + ) + } +} + +PullRefreshConvertProps.propTypes = { + pullRefreshProps: PropTypes.object +} + +PullRefreshConvertProps.defaultProps = { + pullRefreshProps: null +} + +export { PullRefreshConvertProps as default, Indicator, PullRefresh } diff --git a/src/spring.js b/src/spring.js index acd6984..b512847 100644 --- a/src/spring.js +++ b/src/spring.js @@ -2,7 +2,7 @@ import EventEmitter from 'event-emitter' const sleep = msec => new Promise(resolve => setTimeout(resolve, msec)) const loop = async promise => { - const proc = async () => await promise() && await proc() + const proc = async () => (await promise()) && (await proc()) await proc() } @@ -38,7 +38,7 @@ export default class Spring { return this._value } setValue(value) { - if(this._value !== value) { + if (this._value !== value) { this._value = value this._onUpdate(this) } @@ -49,23 +49,32 @@ export default class Spring { async _wait(type) { await new Promise(resolve => this._emitter.once(type, resolve)) } - async loop() { - if(this._loop) return + loop() { + if (this._loop) return this._emit('start') this._loop = true - await loop(async () => { - await sleep(1000 / 60) - if(this._paused) return true + const _rafTimeout = (proc) => { + setTimeout(proc, 1000 / 60) + } + + const raf = requestAnimationFrame ? requestAnimationFrame : _rafTimeout + + const loop = () => { + if (this._paused) return true // TODO: dummy -> use tention,friction - const dv = (this._endValue - this._value) / 5 + const dv = (this._endValue - this._value) / 2 this.setValue(this._value + dv) - return Math.abs(dv) > 0.2 - }) - this.setValue(this._endValue) + if (Math.abs(dv) > 0.5) { + raf(loop) + } else { + this.setValue(this._endValue) - this._loop = false - this._emit('end') + this._loop = false + this._emit('end') + } + } + raf(loop) } }