├── .babelrc ├── .gitignore ├── README-cn.md ├── README.md ├── example ├── App.css ├── App1.jsx ├── App2.jsx ├── App3.jsx ├── App4.jsx ├── README.md ├── index1.html ├── index2.html ├── index3.html └── index4.html ├── gulpfile.js ├── lib ├── FooterNode.js ├── HeadNode.js ├── ReactPullLoad.js ├── ReactPullLoad.less ├── constants.js └── index.js ├── package.json ├── postcss.config.js ├── src ├── FooterNode.jsx ├── HeadNode.jsx ├── ReactPullLoad.jsx ├── ReactPullLoad.less ├── constants.js ├── index.d.ts └── index.js ├── webpack.config.example.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-1", "react"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | package-lock.json 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # Typescript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | demo 62 | 63 | deploy.config.json 64 | 65 | dist 66 | 67 | .publish 68 | -------------------------------------------------------------------------------- /README-cn.md: -------------------------------------------------------------------------------- 1 | # [English](./README.md) 2 | 3 | # [react-pullLoad](https://github.com/react-ld/react-pullLoad) 4 | 5 | React 版本的 [pullLoad](https://github.com/lidianhao123/pullLoad) 下拉更新 上拉加载更多 组件 6 | 7 | [pullLoad](https://github.com/lidianhao123/pullLoad) 非 react 版本,支持 require.js 模块化调用 8 | 9 | #### 示例 10 | 11 | [demo1](https://react-ld.github.io/react-pullLoad/index1.html) ReactPullLoad 根节点 DOM 作为容器 12 | 13 | [demo2](https://react-ld.github.io/react-pullLoad/index2.html) ReactPullLoad 根节点 DOM 作为容器 14 | 15 | [demo3](https://react-ld.github.io/react-pullLoad/index3.html) document.body 作为容器 且自定义刷新和加载更多 UI 组件 16 | 17 | [demo4](https://react-ld.github.io/react-pullLoad/index4.html) 禁用下拉刷新功能 18 | 19 | # 当前版本 1.2.0 20 | 21 | 支持 Typescript 22 | 23 | # 简介 24 | 25 | 1. 只依赖 react/react-dom 26 | 2. 样式使用 less 编写 27 | 3. 支持 body 或者组件根 DOM 固定高度作为外部容器 contianer(即可视区域大小) 28 | 4. 触摸事件绑定在内容块 content(即高度为 auto 的 DOM ) 29 | 5. 纯 React 组件方式开发的 30 | 6. 支持自定义刷新和加载更多 UI 组件 31 | 7. 支持代码动态调起刷新和加载更多(组件将展示刷新和加载更多样式) 32 | 8. **只支持移动触摸设备** 33 | 34 | # 功能点 35 | 36 | 1. 下拉距离大于阈值触发刷新动作 37 | 2. 滚动到距底部距离小于阈值加载更多 38 | 3. 支持自定义刷新和加载更多 UI 组件 39 | 40 | # 使用说明 41 | 42 | ```sh 43 | npm install --save react-pullload 44 | ``` 45 | 46 | ```js 47 | import ReactPullLoad, { STATS } from "react-pullload"; 48 | import "node_modules/react-pullload/dist/ReactPullLoad.css"; 49 | 50 | export class App extends Component { 51 | constructor() { 52 | super(); 53 | this.state = { 54 | hasMore: true, 55 | data: cData, 56 | action: STATS.init, 57 | index: loadMoreLimitNum //loading more test time limit 58 | }; 59 | } 60 | 61 | handleAction = action => { 62 | console.info(action, this.state.action, action === this.state.action); 63 | //new action must do not equel to old action 64 | if (action === this.state.action) { 65 | return false; 66 | } 67 | 68 | if (action === STATS.refreshing) { 69 | //刷新 70 | this.handRefreshing(); 71 | } else if (action === STATS.loading) { 72 | //加载更多 73 | this.handLoadMore(); 74 | } else { 75 | //DO NOT modify below code 76 | this.setState({ 77 | action: action 78 | }); 79 | } 80 | }; 81 | 82 | handRefreshing = () => { 83 | if (STATS.refreshing === this.state.action) { 84 | return false; 85 | } 86 | 87 | setTimeout(() => { 88 | //refreshing complete 89 | this.setState({ 90 | data: cData, 91 | hasMore: true, 92 | action: STATS.refreshed, 93 | index: loadMoreLimitNum 94 | }); 95 | }, 3000); 96 | 97 | this.setState({ 98 | action: STATS.refreshing 99 | }); 100 | }; 101 | 102 | handLoadMore = () => { 103 | if (STATS.loading === this.state.action) { 104 | return false; 105 | } 106 | //无更多内容则不执行后面逻辑 107 | if (!this.state.hasMore) { 108 | return; 109 | } 110 | 111 | setTimeout(() => { 112 | if (this.state.index === 0) { 113 | this.setState({ 114 | action: STATS.reset, 115 | hasMore: false 116 | }); 117 | } else { 118 | this.setState({ 119 | data: [...this.state.data, cData[0], cData[0]], 120 | action: STATS.reset, 121 | index: this.state.index - 1 122 | }); 123 | } 124 | }, 3000); 125 | 126 | this.setState({ 127 | action: STATS.loading 128 | }); 129 | }; 130 | 131 | render() { 132 | const { data, hasMore } = this.state; 133 | 134 | const fixHeaderStyle = { 135 | position: "fixed", 136 | width: "100%", 137 | height: "50px", 138 | color: "#fff", 139 | lineHeight: "50px", 140 | backgroundColor: "#e24f37", 141 | left: 0, 142 | top: 0, 143 | textAlign: "center", 144 | zIndex: 1 145 | }; 146 | 147 | return ( 148 |
149 |
fixed header
150 | 158 | 169 | 170 |
171 | ); 172 | } 173 | } 174 | ``` 175 | 176 | # 参数说明: 177 | 178 | | 参数 | 说明 | 类型 | 默认值 | 备注 | 179 | | ---------------- | --------------------------------------------- | ------ | ------------------------------------------------ | --------------------- | 180 | | action | 用于同步状态 | string | | isRequired | 181 | | handleAction | 用于处理状态 | func | | isRequired | 182 | | hasMore | 是否还有更多内容可加载 | bool | false | | 183 | | downEnough | 下拉距离是否满足要求 | num | 100 | | 184 | | distanceBottom | 距离底部距离触发加载更多 | num | 100 | | 185 | | isBlockContainer | 是否开启使用组件根 DOM 作为外部容器 contianer | bool | false | | 186 | | HeadNode | 自定义顶部刷新 UI 组件 | any | [ReactPullLoad HeadNode](./src/HeadNode.jsx) | 必须是一个 React 组件 | 187 | | FooterNode | 自定义底部加载更多 UI 组件 | any | [ReactPullLoad FooterNode](./src/FooterNode.jsx) | 必须是一个 React 组件 | 188 | 189 | 另外 ReactPullLoad 组件支持根属性扩展 例如: className\style 等等 190 | 191 | # STATS list 192 | 193 | | 属性 | 值 | 根节点 className | 说明 | 194 | | ---------- | ---------------- | -------------------- | -------------------- | 195 | | init | '' | | 组件初始状态 | 196 | | pulling | 'pulling' | state-pulling | 下拉状态 | 197 | | enough | 'pulling enough' | state-pulling.enough | 下拉并且已经满足阈值 | 198 | | refreshing | 'refreshing' | state-refreshing | 刷新中(加载数据中) | 199 | | refreshed | 'refreshed' | state-refreshed | 完成刷新动作 | 200 | | reset | 'reset' | state-reset | 恢复默认状态 | 201 | | loading | 'loading' | state-loading | 加载中 | 202 | 203 | init/reset -> pulling -> enough -> refreshing -> refreshed -> reset 204 | 205 | init/reset -> pulling -> reset 206 | 207 | init/reset -> loading -> reset 208 | 209 | # 自定义刷新及加载组件 210 | 211 | 请参考默认刷新及加载组件源码(通过 css 根节点不同 className 设置对应 UI 样式来实现) 212 | 213 | [ReactPullLoad HeadNode](./src/HeadNode.jsx) 214 | 215 | [ReactPullLoad FooterNode](./src/FooterNode.jsx) 216 | 217 | 或参考 demo3 中的实现方式在组件内容通过获取的 loaderState 与 STATS 不同状态对比来现实 218 | 219 | [demo3](https://react-ld.github.io/react-pullLoad/index3.html) 220 | 221 | # License 222 | 223 | MIT 224 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [中文](./README-cn.md) 2 | 3 | # [react-pullLoad](https://github.com/react-ld/react-pullLoad) 4 | 5 | Refreshing and Loading more component for react. 6 | 7 | [pullLoad](https://github.com/lidianhao123/pullLoad) is another refreshing and loading more lib without react, support require.js to load lib. 8 | 9 | #### examples 10 | 11 | [demo1](https://react-ld.github.io/react-pullLoad/index1.html) use ReactPullLoad root DOM as container 12 | 13 | [demo2](https://react-ld.github.io/react-pullLoad/index2.html) use ReactPullLoad root DOM as container 14 | 15 | [demo3](https://react-ld.github.io/react-pullLoad/index3.html) use document.body as container, and config UI component (HeadNode and FooterNode). 16 | 17 | [demo4](https://react-ld.github.io/react-pullLoad/index4.html) forbidden pull refresh 18 | 19 | # version 1.2.0 20 | 21 | Support Typescript 22 | 23 | # Description 24 | 25 | 1. Only depend on react/react-dom, without any other package. 26 | 2. Use less. 27 | 3. Support body or root Dom as container. 28 | 4. Bind touch event on component root Dom. 29 | 5. It.s develop as Pure react component. 30 | 6. Support config UI component (HeadNode and FooterNode). 31 | 7. Can apply refreshing or loading through modify STATE. 32 | 8. **Only support mobile device** 33 | 34 | # How to use 35 | 36 | ```sh 37 | npm install --save react-pullload 38 | ``` 39 | 40 | ```js 41 | import ReactPullLoad, { STATS } from "react-pullload"; 42 | import "node_modules/react-pullload/dist/ReactPullLoad.css"; 43 | 44 | export class App extends Component { 45 | constructor() { 46 | super(); 47 | this.state = { 48 | hasMore: true, 49 | data: cData, 50 | action: STATS.init, 51 | index: loadMoreLimitNum //loading more test time limit 52 | }; 53 | } 54 | 55 | handleAction = action => { 56 | console.info(action, this.state.action, action === this.state.action); 57 | //new action must do not equel to old action 58 | if (action === this.state.action) { 59 | return false; 60 | } 61 | 62 | if (action === STATS.refreshing) { 63 | this.handRefreshing(); 64 | } else if (action === STATS.loading) { 65 | this.handLoadMore(); 66 | } else { 67 | //DO NOT modify below code 68 | this.setState({ 69 | action: action 70 | }); 71 | } 72 | }; 73 | 74 | handRefreshing = () => { 75 | if (STATS.refreshing === this.state.action) { 76 | return false; 77 | } 78 | 79 | setTimeout(() => { 80 | //refreshing complete 81 | this.setState({ 82 | data: cData, 83 | hasMore: true, 84 | action: STATS.refreshed, 85 | index: loadMoreLimitNum 86 | }); 87 | }, 3000); 88 | 89 | this.setState({ 90 | action: STATS.refreshing 91 | }); 92 | }; 93 | 94 | handLoadMore = () => { 95 | if (STATS.loading === this.state.action) { 96 | return false; 97 | } 98 | //无更多内容则不执行后面逻辑 99 | if (!this.state.hasMore) { 100 | return; 101 | } 102 | 103 | setTimeout(() => { 104 | if (this.state.index === 0) { 105 | this.setState({ 106 | action: STATS.reset, 107 | hasMore: false 108 | }); 109 | } else { 110 | this.setState({ 111 | data: [...this.state.data, cData[0], cData[0]], 112 | action: STATS.reset, 113 | index: this.state.index - 1 114 | }); 115 | } 116 | }, 3000); 117 | 118 | this.setState({ 119 | action: STATS.loading 120 | }); 121 | }; 122 | 123 | render() { 124 | const { data, hasMore } = this.state; 125 | 126 | const fixHeaderStyle = { 127 | position: "fixed", 128 | width: "100%", 129 | height: "50px", 130 | color: "#fff", 131 | lineHeight: "50px", 132 | backgroundColor: "#e24f37", 133 | left: 0, 134 | top: 0, 135 | textAlign: "center", 136 | zIndex: 1 137 | }; 138 | 139 | return ( 140 |
141 |
fixed header
142 | 150 | 161 | 162 |
163 | ); 164 | } 165 | } 166 | ``` 167 | 168 | # API: 169 | 170 | | Property | Description | Type | default | Remarks | 171 | | ---------------- | ------------------------------------------- | ------ | ------------------------------------------------ | ------------------------- | 172 | | action | sync component status | string | | isRequired | 173 | | handleAction | handle status | func | | isRequired | 174 | | hasMore | flag for are there any more content to load | bool | false | | 175 | | downEnough | how long distance is enough to refreshing | num | 100 | use px as unit | 176 | | distanceBottom | current position is apart from bottom | num | 100 | use px as unit | 177 | | isBlockContainer | set root dom as container | bool | false | | 178 | | HeadNode | custom header UI compoent | any | [ReactPullLoad HeadNode](./src/HeadNode.jsx) | must be a react component | 179 | | FooterNode | custom footer UI compoent | any | [ReactPullLoad FooterNode](./src/FooterNode.jsx) | must be a react component | 180 | 181 | Remarks: ReactPullLoad support set root dom className and style. 182 | 183 | # STATS list 184 | 185 | | Property | Value | root className | explain | 186 | | ---------- | ---------------- | -------------------- | ---------------------------- | 187 | | init | '' | | component initial status | 188 | | pulling | 'pulling' | state-pulling | pull status | 189 | | enough | 'pulling enough' | state-pulling.enough | pull down enough status | 190 | | refreshing | 'refreshing' | state-refreshing | refreshing status fetch data | 191 | | refreshed | 'refreshed' | state-refreshed | refreshed | 192 | | reset | 'reset' | state-reset | reset status | 193 | | loading | 'loading' | state-loading | fetching data | 194 | 195 | init/reset -> pulling -> enough -> refreshing -> refreshed -> reset 196 | 197 | init/reset -> pulling -> reset 198 | 199 | init/reset -> loading -> reset 200 | 201 | # Custom UI components 202 | 203 | Please refer to the default HeadNode and FooterNode components 204 | 205 | [ReactPullLoad HeadNode](./src/HeadNode.jsx) 206 | 207 | [ReactPullLoad FooterNode](./src/FooterNode.jsx) 208 | 209 | Or refer to demo3, show different dom style through compare props loaderState width STATS. 210 | 211 | [demo3](https://react-ld.github.io/react-pullLoad/index3.html) 212 | 213 | # License 214 | 215 | MIT 216 | -------------------------------------------------------------------------------- /example/App.css: -------------------------------------------------------------------------------- 1 | html,body{margin: 0; padding: 0;} 2 | li{font-size: 20px; width: 100%;list-style: none;} 3 | img{width: 100%;} 4 | div, .test-ul, p{margin: 0; padding: 0;} 5 | .block{position: absolute; top:0; left:0; box-sizing: border-box; height: 100%;box-sizing: border-box;} 6 | 7 | button{ 8 | display: inline-block; 9 | font-weight: 500; 10 | text-align: center; 11 | -ms-touch-action: manipulation; 12 | touch-action: manipulation; 13 | cursor: pointer; 14 | background-image: none; 15 | border: 1px solid transparent; 16 | white-space: nowrap; 17 | line-height: 1.5; 18 | padding: 4px 15px; 19 | font-size: 12px; 20 | border-radius: 4px; 21 | -webkit-user-select: none; 22 | -moz-user-select: none; 23 | -ms-user-select: none; 24 | user-select: none; 25 | -webkit-transition: all .3s cubic-bezier(.645,.045,.355,1); 26 | transition: all .3s cubic-bezier(.645,.045,.355,1); 27 | position: relative; 28 | color: rgba(0,0,0,.65); 29 | background-color: #fff; 30 | border-color: #d9d9d9; 31 | outline: 0; 32 | margin-right: 8px; 33 | margin-bottom: 12px; 34 | margin-top: 12px; 35 | -webkit-appearance: button; 36 | box-sizing: border-box; 37 | } -------------------------------------------------------------------------------- /example/App1.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component, PureComponent } from 'react' 3 | import PropTypes from 'prop-types'; 4 | import { render } from 'react-dom' 5 | import ReactPullLoad,{STATS} from 'index.js' 6 | import '../src/ReactPullLoad.less' 7 | import './App.css' 8 | 9 | 10 | const defaultStyle ={ 11 | width: "100%", 12 | textAlign: "center", 13 | fontSize: "20px", 14 | lineHeight: "1.5" 15 | } 16 | 17 | const loadMoreLimitNum = 2; 18 | 19 | const cData = [ 20 | "http://img1.gtimg.com/15/1580/158031/15803178_1200x1000_0.jpg", 21 | "http://img1.gtimg.com/15/1580/158031/15803179_1200x1000_0.jpg", 22 | "http://img1.gtimg.com/15/1580/158031/15803181_1200x1000_0.jpg", 23 | "http://img1.gtimg.com/15/1580/158031/15803182_1200x1000_0.jpg", 24 | "http://img1.gtimg.com/15/1580/158031/15803183_1200x1000_0.jpg", 25 | // "http://img1.gtimg.com/15/1580/158031/15803184_1200x1000_0.jpg", 26 | // "http://img1.gtimg.com/15/1580/158031/15803186_1200x1000_0.jpg" 27 | ] 28 | 29 | export class App extends Component{ 30 | constructor(){ 31 | super(); 32 | this.state ={ 33 | hasMore: true, 34 | data: cData, 35 | action: STATS.init, 36 | index: loadMoreLimitNum //loading more test time limit 37 | } 38 | } 39 | 40 | handleAction = (action) => { 41 | console.info(action, this.state.action,action === this.state.action); 42 | //new action must do not equel to old action 43 | if(action === this.state.action || 44 | action === STATS.refreshing && this.state.action === STATS.loading || 45 | action === STATS.loading && this.state.action === STATS.refreshing){ 46 | // console.info("It's same action or on loading or on refreshing ",action, this.state.action,action === this.state.action); 47 | return false 48 | } 49 | 50 | if(action === STATS.refreshing){//刷新 51 | setTimeout(()=>{ 52 | //refreshing complete 53 | this.setState({ 54 | data: cData, 55 | hasMore: true, 56 | action: STATS.refreshed, 57 | index: loadMoreLimitNum 58 | }); 59 | }, 3000) 60 | } else if(action === STATS.loading){//加载更多 61 | this.setState({ 62 | hasMore: true 63 | }); 64 | setTimeout(()=>{ 65 | if(this.state.index === 0){ 66 | this.setState({ 67 | action: STATS.reset, 68 | hasMore: false 69 | }); 70 | } else{ 71 | this.setState({ 72 | data: [...this.state.data, cData[0], cData[0]], 73 | action: STATS.reset, 74 | index: this.state.index - 1 75 | }); 76 | } 77 | }, 3000) 78 | } 79 | 80 | //DO NOT modify below code 81 | this.setState({ 82 | action: action 83 | }) 84 | } 85 | 86 | getScrollTop = ()=>{ 87 | if(this.refs.reactpullload){ 88 | console.info(this.refs.reactpullload.getScrollTop()); 89 | } 90 | } 91 | setScrollTop = ()=>{ 92 | if(this.refs.reactpullload){ 93 | console.info(this.refs.reactpullload.setScrollTop(100)); 94 | } 95 | } 96 | 97 | render(){ 98 | const { 99 | data, 100 | hasMore 101 | } = this.state 102 | 103 | const fixHeaderStyle = { 104 | position: "fixed", 105 | width: "100%", 106 | height: "50px", 107 | color: "#fff", 108 | lineHeight: "50px", 109 | backgroundColor: "#e24f37", 110 | left: 0, 111 | top: 0, 112 | textAlign: "center", 113 | zIndex: 1 114 | } 115 | 116 | const fixButtonStyle = { 117 | position: "fixed", 118 | top: 200, 119 | width: "100%", 120 | } 121 | 122 | return ( 123 |
124 |
125 | fixed header 126 |
127 | 137 | 150 | 151 |
152 | ) 153 | } 154 | } 155 | 156 | render( 157 | , 158 | document.getElementById('root') 159 | ) -------------------------------------------------------------------------------- /example/App2.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component, PureComponent } from 'react' 3 | import PropTypes from 'prop-types'; 4 | import { render } from 'react-dom' 5 | import ReactPullLoad,{STATS} from 'index.js' 6 | import '../src/ReactPullLoad.less' 7 | import './App.css' 8 | 9 | const defaultStyle ={ 10 | width: "100%", 11 | textAlign: "center", 12 | fontSize: "20px", 13 | lineHeight: "1.5" 14 | } 15 | 16 | const loadMoreLimitNum = 2; 17 | 18 | const cData = [ 19 | "http://img1.gtimg.com/15/1580/158031/15803178_1200x1000_0.jpg", 20 | "http://img1.gtimg.com/15/1580/158031/15803179_1200x1000_0.jpg", 21 | "http://img1.gtimg.com/15/1580/158031/15803181_1200x1000_0.jpg", 22 | "http://img1.gtimg.com/15/1580/158031/15803182_1200x1000_0.jpg", 23 | "http://img1.gtimg.com/15/1580/158031/15803183_1200x1000_0.jpg", 24 | // "http://img1.gtimg.com/15/1580/158031/15803184_1200x1000_0.jpg", 25 | // "http://img1.gtimg.com/15/1580/158031/15803186_1200x1000_0.jpg" 26 | ] 27 | 28 | export class App extends Component{ 29 | constructor(){ 30 | super(); 31 | this.state ={ 32 | hasMore: true, 33 | data: cData, 34 | action: STATS.init, 35 | index: loadMoreLimitNum //loading more test time limit 36 | } 37 | } 38 | 39 | handleAction = (action) => { 40 | console.info(action, this.state.action,action === this.state.action); 41 | //new action must do not equel to old action 42 | if(action === this.state.action || 43 | action === STATS.refreshing && this.state.action === STATS.loading || 44 | action === STATS.loading && this.state.action === STATS.refreshing){ 45 | console.info("It's same action or on loading or on refreshing ",action, this.state.action,action === this.state.action); 46 | return false 47 | } 48 | 49 | if(action === STATS.refreshing){//刷新 50 | setTimeout(()=>{ 51 | //refreshing complete 52 | this.setState({ 53 | data: cData, 54 | hasMore: true, 55 | action: STATS.refreshed, 56 | index: loadMoreLimitNum 57 | }); 58 | }, 3000) 59 | } else if(action === STATS.loading){//加载更多 60 | this.setState({ 61 | hasMore: true 62 | }); 63 | setTimeout(()=>{ 64 | if(this.state.index === 0){ 65 | this.setState({ 66 | action: STATS.reset, 67 | hasMore: false 68 | }); 69 | } else{ 70 | this.setState({ 71 | data: [...this.state.data, cData[0], cData[0]], 72 | action: STATS.reset, 73 | index: this.state.index - 1 74 | }); 75 | } 76 | }, 3000) 77 | } 78 | 79 | //DO NOT modify below code 80 | this.setState({ 81 | action: action 82 | }) 83 | } 84 | 85 | render(){ 86 | const { 87 | data, 88 | hasMore 89 | } = this.state 90 | 91 | return ( 92 |
93 | 101 |
    102 | 103 | 104 | { 105 | data.map( (str, index )=>{ 106 | return
  • 107 | }) 108 | } 109 |
110 |
111 |
112 | ) 113 | } 114 | } 115 | 116 | render( 117 | , 118 | document.getElementById('root') 119 | ) -------------------------------------------------------------------------------- /example/App3.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component, PureComponent } from 'react' 3 | import PropTypes from 'prop-types'; 4 | import { render } from 'react-dom' 5 | import ReactPullLoad,{STATS} from 'index.js' 6 | import '../src/ReactPullLoad.less' 7 | import './App.css' 8 | 9 | const defaultStyle ={ 10 | width: "100%", 11 | textAlign: "center", 12 | fontSize: "20px", 13 | lineHeight: "1.5" 14 | } 15 | 16 | class HeadNode extends PureComponent{ 17 | 18 | static propTypes = { 19 | loaderState: PropTypes.string.isRequired, 20 | }; 21 | 22 | static defaultProps = { 23 | loaderState: STATS.init, 24 | }; 25 | 26 | render(){ 27 | const { 28 | loaderState 29 | } = this.props 30 | 31 | let content = "" 32 | if(loaderState == STATS.pulling){ 33 | content = "下拉刷新" 34 | } else if(loaderState == STATS.enough){ 35 | content = "松开刷新" 36 | } else if(loaderState == STATS.refreshing){ 37 | content = "正在刷新..." 38 | } else if(loaderState == STATS.refreshed){ 39 | content = "刷新成功" 40 | } 41 | 42 | return( 43 |
44 | {content} 45 |
46 | ) 47 | } 48 | } 49 | 50 | class FooterNode extends PureComponent{ 51 | 52 | static propTypes = { 53 | loaderState: PropTypes.string.isRequired, 54 | hasMore: PropTypes.bool.isRequired 55 | }; 56 | 57 | static defaultProps = { 58 | loaderState: STATS.init, 59 | hasMore: true 60 | }; 61 | 62 | render(){ 63 | const { 64 | loaderState, 65 | hasMore 66 | } = this.props 67 | 68 | let content = "" 69 | // if(hasMore === false){ 70 | // content = "没有更多" 71 | // } else if(loaderState == STATS.loading && hasMore === true){ 72 | // content = "加载中" 73 | // } 74 | if(loaderState == STATS.loading){ 75 | content = "加载中" 76 | } else if(hasMore === false){ 77 | content = "没有更多" 78 | } 79 | 80 | return( 81 |
82 | {content} 83 |
84 | ) 85 | } 86 | } 87 | 88 | const loadMoreLimitNum = 2; 89 | 90 | const cData = [ 91 | "http://img1.gtimg.com/15/1580/158031/15803178_1200x1000_0.jpg", 92 | "http://img1.gtimg.com/15/1580/158031/15803179_1200x1000_0.jpg", 93 | "http://img1.gtimg.com/15/1580/158031/15803181_1200x1000_0.jpg", 94 | "http://img1.gtimg.com/15/1580/158031/15803182_1200x1000_0.jpg", 95 | "http://img1.gtimg.com/15/1580/158031/15803183_1200x1000_0.jpg", 96 | // "http://img1.gtimg.com/15/1580/158031/15803184_1200x1000_0.jpg", 97 | // "http://img1.gtimg.com/15/1580/158031/15803186_1200x1000_0.jpg" 98 | ] 99 | 100 | export class App extends Component{ 101 | constructor(){ 102 | super(); 103 | this.state ={ 104 | hasMore: true, 105 | data: cData, 106 | action: STATS.init, 107 | index: loadMoreLimitNum //loading more test time limit 108 | } 109 | } 110 | 111 | handleAction = (action) => { 112 | //new action must do not equel to old action 113 | if(action === this.state.action || 114 | action === STATS.refreshing && this.state.action === STATS.loading || 115 | action === STATS.loading && this.state.action === STATS.refreshing){ 116 | console.info("It's same action or on loading or on refreshing ",action, this.state.action,action === this.state.action); 117 | return false 118 | } 119 | 120 | if(action === STATS.refreshing){//刷新 121 | setTimeout(()=>{ 122 | //refreshing complete 123 | this.setState({ 124 | data: cData, 125 | hasMore: true, 126 | action: STATS.refreshed, 127 | index: loadMoreLimitNum 128 | }); 129 | }, 3000) 130 | } else if(action === STATS.loading && this.state.hasMore){//加载更多 131 | setTimeout(()=>{ 132 | if(this.state.index === 0){ 133 | this.setState({ 134 | action: STATS.reset, 135 | hasMore: false 136 | }); 137 | } else{ 138 | this.setState({ 139 | data: [...this.state.data, cData[0], cData[0]], 140 | action: STATS.reset, 141 | index: this.state.index - 1 142 | }); 143 | } 144 | }, 3000) 145 | } 146 | 147 | //无更多内容,不再加载数据 148 | if(action === STATS.loading && !this.state.hasMore){ 149 | return; 150 | } 151 | //DO NOT modify below code 152 | this.setState({ 153 | action: action 154 | }) 155 | } 156 | 157 | render(){ 158 | const { 159 | data, 160 | hasMore 161 | } = this.state 162 | 163 | const fixHeaderStyle = { 164 | position: "fixed", 165 | width: "100%", 166 | height: "50px", 167 | color: "#fff", 168 | lineHeight: "50px", 169 | backgroundColor: "#e24f37", 170 | left: 0, 171 | top: 0, 172 | textAlign: "center", 173 | zIndex: 1 174 | } 175 | 176 | return ( 177 |
178 |
179 | fixed header 180 |
181 | 190 |
    191 | 192 | 193 | { 194 | data.map( (str, index )=>{ 195 | return
  • 196 | }) 197 | } 198 |
199 |
200 |
201 | ) 202 | } 203 | } 204 | 205 | render( 206 | , 207 | document.getElementById('root') 208 | ) -------------------------------------------------------------------------------- /example/App4.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component, PureComponent } from 'react' 3 | import PropTypes from 'prop-types'; 4 | import { render } from 'react-dom' 5 | import ReactPullLoad,{STATS} from 'index.js' 6 | import '../src/ReactPullLoad.less' 7 | import './App.css' 8 | 9 | const defaultStyle ={ 10 | width: "100%", 11 | textAlign: "center", 12 | fontSize: "20px", 13 | lineHeight: "1.5" 14 | } 15 | 16 | const loadMoreLimitNum = 2; 17 | 18 | const cData = [ 19 | "http://img1.gtimg.com/15/1580/158031/15803178_1200x1000_0.jpg", 20 | "http://img1.gtimg.com/15/1580/158031/15803179_1200x1000_0.jpg", 21 | "http://img1.gtimg.com/15/1580/158031/15803181_1200x1000_0.jpg", 22 | "http://img1.gtimg.com/15/1580/158031/15803182_1200x1000_0.jpg", 23 | "http://img1.gtimg.com/15/1580/158031/15803183_1200x1000_0.jpg", 24 | // "http://img1.gtimg.com/15/1580/158031/15803184_1200x1000_0.jpg", 25 | // "http://img1.gtimg.com/15/1580/158031/15803186_1200x1000_0.jpg" 26 | ] 27 | 28 | export class App extends Component{ 29 | constructor(){ 30 | super(); 31 | this.state ={ 32 | hasMore: true, 33 | data: cData, 34 | action: STATS.init, 35 | index: loadMoreLimitNum //loading more test time limit 36 | } 37 | } 38 | 39 | handleAction = (action) => { 40 | console.info(action, this.state.action,action === this.state.action); 41 | if(action !== STATS.loading){ 42 | return false; 43 | } 44 | 45 | this.setState({ 46 | hasMore: true 47 | }); 48 | setTimeout(()=>{ 49 | if(this.state.index === 0){ 50 | this.setState({ 51 | action: STATS.reset, 52 | hasMore: false 53 | }); 54 | } else{ 55 | this.setState({ 56 | data: [...this.state.data, cData[0], cData[0]], 57 | action: STATS.reset, 58 | index: this.state.index - 1 59 | }); 60 | } 61 | }, 3000) 62 | 63 | //DO NOT modify below code 64 | this.setState({ 65 | action: action 66 | }) 67 | } 68 | 69 | getScrollTop = ()=>{ 70 | if(this.refs.reactpullload){ 71 | console.info(this.refs.reactpullload.getScrollTop()); 72 | } 73 | } 74 | setScrollTop = ()=>{ 75 | if(this.refs.reactpullload){ 76 | console.info(this.refs.reactpullload.setScrollTop(100)); 77 | } 78 | } 79 | 80 | render(){ 81 | const { 82 | data, 83 | hasMore 84 | } = this.state 85 | 86 | const fixHeaderStyle = { 87 | position: "fixed", 88 | width: "100%", 89 | height: "50px", 90 | color: "#fff", 91 | lineHeight: "50px", 92 | backgroundColor: "#e24f37", 93 | left: 0, 94 | top: 0, 95 | textAlign: "center", 96 | zIndex: 1 97 | } 98 | 99 | const fixButtonStyle = { 100 | position: "fixed", 101 | top: 200, 102 | width: "100%", 103 | } 104 | 105 | return ( 106 |
107 |
108 | fixed header 109 |
110 | 120 |
    121 | 122 | 123 |
    124 | 125 | 126 |
    127 | { 128 | data.map( (str, index )=>{ 129 | return
  • 130 | }) 131 | } 132 |
133 |
134 |
135 | ) 136 | } 137 | } 138 | 139 | render( 140 | , 141 | document.getElementById('root') 142 | ) -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # 示例 2 | [demo1](https://react-ld.github.io/react-pullLoad/index1.html) ReactPullLoad 根节点 DOM 作为容器 3 | 4 | [demo2](https://react-ld.github.io/react-pullLoad/index2.html) ReactPullLoad 根节点 DOM 作为容器 5 | 6 | [demo3](https://react-ld.github.io/react-pullLoad/index3.html) document.body 作为容器 且自定义刷新和加载更多 UI 组件 7 | 8 | [demo4](https://react-ld.github.io/react-pullLoad/index4.html) 禁用下拉刷新功能 9 | 10 | # 文档 11 | 12 | [react-pullLoad](https://github.com/react-ld/react-pullLoad) -------------------------------------------------------------------------------- /example/index1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ReactPullLoad demo1 6 | 7 | 8 | 9 |
10 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/index2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ReactPullLoad demo2 6 | 7 | 8 | 9 |
10 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/index3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ReactPullLoad demo3 6 | 7 | 8 | 9 |
10 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/index4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ReactPullLoad demo4 6 | 7 | 8 | 9 |
10 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var webpack = require('webpack'); 3 | var clean = require('gulp-clean'); 4 | var gutil = require('gulp-util'); 5 | // var ftp = require( 'vinyl-ftp' ); 6 | var ghPages = require('gulp-gh-pages'); 7 | // var deploy = require('./deploy.config.json'); 8 | var deploy_remote_path = "/public/17zt/viewer" 9 | var webpack_config_demo = require('./webpack.config.example.js'); 10 | var babel = require('gulp-babel'); 11 | var less = require('gulp-less'); 12 | var path = require('path'); 13 | 14 | gulp.task('demo:clean', function(){ 15 | return gulp.src('./demo', {read: false}) 16 | .pipe(clean()); 17 | }) 18 | 19 | gulp.task('demo:file', ['demo:clean'], function(){ 20 | return gulp.src(['example/**/*.html','example/README.md']) 21 | .pipe(gulp.dest('demo/')) 22 | }) 23 | 24 | //编译示例 25 | gulp.task('demo:webpack', ['demo:clean'], function(callback) { 26 | webpack(webpack_config_demo, function (error,status) { 27 | //gulp 异步任务必须明确执行 callback() 否则 gulp 将一直卡住 28 | callback() 29 | }); 30 | }); 31 | 32 | gulp.task('demo:build', ['demo:file', 'demo:webpack']); 33 | 34 | //部署示例到自己的测试服务器 35 | // gulp.task('deploy:demo', ['build:demo'], function () { 36 | // deploy.log = gutil.log; 37 | 38 | // var conn = ftp.create(deploy); 39 | 40 | // return gulp.src('demo/**') 41 | // .pipe(conn.dest(deploy_remote_path)) 42 | // }) 43 | 44 | //部署示例到 gh-pages 45 | gulp.task('deploy:gh-pages', ['demo:build'], function() { 46 | return gulp.src('./demo/**') 47 | .pipe(ghPages()); 48 | }); 49 | 50 | gulp.task("publish:clean", function(){ 51 | return gulp.src('./dist', {read: false}) 52 | .pipe(clean()); 53 | }) 54 | 55 | gulp.task("publish:ts", ["publish:clean"], function(){ 56 | return gulp.src('src/**/*.ts') 57 | .pipe(gulp.dest('dist')); 58 | }) 59 | 60 | //编译 js 文件 61 | gulp.task('publish:js', ["publish:clean"], function(){ 62 | return gulp.src('src/**/*.{js,jsx}') 63 | .pipe(babel({ 64 | presets: ["es2015", "stage-1", "react"] 65 | })) 66 | .pipe(gulp.dest('dist')); 67 | }) 68 | 69 | //编译 less 文件 70 | gulp.task('publish:less', ["publish:clean"], function () { 71 | return gulp.src('src/**/*.less') 72 | .pipe(less({ 73 | paths: [ path.join(__dirname, 'less', 'includes') ] 74 | })) 75 | .pipe(gulp.dest('dist')); 76 | }); 77 | 78 | //发布 css 文件 79 | gulp.task('publish:css', ["publish:clean"], function(){ 80 | return gulp.src('src/**/*.css') 81 | .pipe(gulp.dest('dist')) 82 | }) 83 | 84 | //打包发布 npm 85 | gulp.task('publish', ["publish:clean", 'publish:ts', 'publish:js', 'publish:less']); 86 | 87 | gulp.task('demo', ['deploy:demo']); 88 | 89 | gulp.task('gh-pages', ['deploy:gh-pages']); -------------------------------------------------------------------------------- /lib/FooterNode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _constants = require('./constants'); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 18 | 19 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 20 | 21 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 22 | 23 | var FooterNode = function (_PureComponent) { 24 | _inherits(FooterNode, _PureComponent); 25 | 26 | function FooterNode() { 27 | _classCallCheck(this, FooterNode); 28 | 29 | return _possibleConstructorReturn(this, (FooterNode.__proto__ || Object.getPrototypeOf(FooterNode)).apply(this, arguments)); 30 | } 31 | 32 | _createClass(FooterNode, [{ 33 | key: 'render', 34 | value: function render() { 35 | var _props = this.props; 36 | var loaderState = _props.loaderState; 37 | var hasMore = _props.hasMore; 38 | 39 | 40 | var className = 'pull-load-footer-default ' + (hasMore ? "" : "nomore"); 41 | 42 | return _react2.default.createElement( 43 | 'div', 44 | { className: className }, 45 | loaderState === _constants.STATS.loading ? _react2.default.createElement('i', null) : "" 46 | ); 47 | } 48 | }]); 49 | 50 | return FooterNode; 51 | }(_react.PureComponent); 52 | 53 | FooterNode.propTypes = { 54 | loaderState: _react.PropTypes.string.isRequired, 55 | hasMore: _react.PropTypes.bool.isRequired 56 | }; 57 | FooterNode.defaultProps = { 58 | loaderState: _constants.STATS.init, 59 | hasMore: true 60 | }; 61 | exports.default = FooterNode; -------------------------------------------------------------------------------- /lib/HeadNode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _constants = require('./constants'); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 18 | 19 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 20 | 21 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 22 | 23 | var HeadNode = function (_PureComponent) { 24 | _inherits(HeadNode, _PureComponent); 25 | 26 | function HeadNode() { 27 | _classCallCheck(this, HeadNode); 28 | 29 | return _possibleConstructorReturn(this, (HeadNode.__proto__ || Object.getPrototypeOf(HeadNode)).apply(this, arguments)); 30 | } 31 | 32 | _createClass(HeadNode, [{ 33 | key: 'render', 34 | value: function render() { 35 | var loaderState = this.props.loaderState; 36 | 37 | 38 | return _react2.default.createElement( 39 | 'div', 40 | { className: 'pull-load-head-default' }, 41 | _react2.default.createElement('i', null) 42 | ); 43 | } 44 | }]); 45 | 46 | return HeadNode; 47 | }(_react.PureComponent); 48 | 49 | HeadNode.propTypes = { 50 | loaderState: _react.PropTypes.string.isRequired 51 | }; 52 | HeadNode.defaultProps = { 53 | loaderState: _constants.STATS.init 54 | }; 55 | exports.default = HeadNode; -------------------------------------------------------------------------------- /lib/ReactPullLoad.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 10 | 11 | var _react = require('react'); 12 | 13 | var _react2 = _interopRequireDefault(_react); 14 | 15 | var _reactDom = require('react-dom'); 16 | 17 | var _constants = require('./constants'); 18 | 19 | var _HeadNode = require('./HeadNode'); 20 | 21 | var _HeadNode2 = _interopRequireDefault(_HeadNode); 22 | 23 | var _FooterNode = require('./FooterNode'); 24 | 25 | var _FooterNode2 = _interopRequireDefault(_FooterNode); 26 | 27 | require('./ReactPullLoad.less'); 28 | 29 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 30 | 31 | function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } 32 | 33 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 34 | 35 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 36 | 37 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 38 | 39 | function addEvent(obj, type, fn) { 40 | if (obj.attachEvent) { 41 | obj['e' + type + fn] = fn; 42 | obj[type + fn] = function () { 43 | obj['e' + type + fn](window.event); 44 | }; 45 | obj.attachEvent('on' + type, obj[type + fn]); 46 | } else obj.addEventListener(type, fn, false); 47 | } 48 | function removeEvent(obj, type, fn) { 49 | if (obj.detachEvent) { 50 | obj.detachEvent('on' + type, obj[type + fn]); 51 | obj[type + fn] = null; 52 | } else obj.removeEventListener(type, fn, false); 53 | } 54 | 55 | var ReactPullLoad = function (_Component) { 56 | _inherits(ReactPullLoad, _Component); 57 | 58 | function ReactPullLoad() { 59 | var _ref; 60 | 61 | var _temp, _this, _ret; 62 | 63 | _classCallCheck(this, ReactPullLoad); 64 | 65 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 66 | args[_key] = arguments[_key]; 67 | } 68 | 69 | return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = ReactPullLoad.__proto__ || Object.getPrototypeOf(ReactPullLoad)).call.apply(_ref, [this].concat(args))), _this), _this.state = { 70 | pullHeight: 0 71 | }, _this.getScrollTop = function () { 72 | if (_this.defaultConfig.container) { 73 | return _this.defaultConfig.container.scrollTop; 74 | } else { 75 | return 0; 76 | } 77 | }, _this.setScrollTop = function (value) { 78 | if (_this.defaultConfig.container) { 79 | var scrollH = _this.defaultConfig.container.scrollHeight; 80 | if (value < 0) { 81 | value = 0; 82 | } 83 | if (value > scrollH) { 84 | value = scrollH; 85 | } 86 | return _this.defaultConfig.container.scrollTop = value; 87 | } else { 88 | return 0; 89 | } 90 | }, _this.easing = function (distance) { 91 | // t: current time, b: begInnIng value, c: change In value, d: duration 92 | var t = distance; 93 | var b = 0; 94 | var d = screen.availHeight; // 允许拖拽的最大距离 95 | var c = d / 2.5; // 提示标签最大有效拖拽距离 96 | 97 | return c * Math.sin(t / d * (Math.PI / 2)) + b; 98 | }, _this.canRefresh = function () { 99 | return [_constants.STATS.refreshing, _constants.STATS.loading].indexOf(_this.props.action) < 0; 100 | }, _this.onPullDownMove = function (data) { 101 | if (!_this.canRefresh()) return false; 102 | 103 | var loaderState = void 0, 104 | diff = data[0].touchMoveY - data[0].touchStartY; 105 | if (diff < 0) { 106 | diff = 0; 107 | } 108 | diff = _this.easing(diff); 109 | if (diff > _this.defaultConfig.downEnough) { 110 | loaderState = _constants.STATS.enough; 111 | } else { 112 | loaderState = _constants.STATS.pulling; 113 | } 114 | _this.setState({ 115 | pullHeight: diff 116 | }); 117 | _this.props.handleAction(loaderState); 118 | }, _this.onPullDownRefresh = function () { 119 | if (!_this.canRefresh()) return false; 120 | 121 | if (_this.props.action === _constants.STATS.pulling) { 122 | _this.setState({ pullHeight: 0 }); 123 | _this.props.handleAction(_constants.STATS.reset); 124 | } else { 125 | _this.setState({ 126 | pullHeight: 0 127 | }); 128 | _this.props.handleAction(_constants.STATS.refreshing); 129 | } 130 | }, _this.onPullUpMove = function (data) { 131 | if (!_this.canRefresh()) return false; 132 | 133 | // const { hasMore, onLoadMore} = this.props 134 | // if (this.props.hasMore) { 135 | _this.setState({ 136 | pullHeight: 0 137 | }); 138 | _this.props.handleAction(_constants.STATS.loading); 139 | // } 140 | }, _this.onTouchStart = function (event) { 141 | var targetEvent = event.changedTouches[0]; 142 | _this.startX = targetEvent.clientX; 143 | _this.startY = targetEvent.clientY; 144 | }, _this.onTouchMove = function (event) { 145 | var scrollTop = _this.defaultConfig.container.scrollTop, 146 | scrollH = _this.defaultConfig.container.scrollHeight, 147 | conH = _this.defaultConfig.container === document.body ? document.documentElement.clientHeight : _this.defaultConfig.container.offsetHeight, 148 | targetEvent = event.changedTouches[0], 149 | curX = targetEvent.clientX, 150 | curY = targetEvent.clientY, 151 | diffX = curX - _this.startX, 152 | diffY = curY - _this.startY; 153 | 154 | //判断垂直移动距离是否大于5 && 横向移动距离小于纵向移动距离 155 | if (Math.abs(diffY) > 5 && Math.abs(diffY) > Math.abs(diffX)) { 156 | //滚动距离小于设定值 &&回调onPullDownMove 函数,并且回传位置值 157 | if (diffY > 5 && scrollTop < _this.defaultConfig.offsetScrollTop) { 158 | //阻止执行浏览器默认动作 159 | event.preventDefault(); 160 | _this.onPullDownMove([{ 161 | touchStartY: _this.startY, 162 | touchMoveY: curY 163 | }]); 164 | } //滚动距离距离底部小于设定值 165 | else if (diffY < 0 && scrollH - scrollTop - conH < _this.defaultConfig.distanceBottom) { 166 | //阻止执行浏览器默认动作 167 | // event.preventDefault(); 168 | _this.onPullUpMove([{ 169 | touchStartY: _this.startY, 170 | touchMoveY: curY 171 | }]); 172 | } 173 | } 174 | }, _this.onTouchEnd = function (event) { 175 | var scrollTop = _this.defaultConfig.container.scrollTop, 176 | targetEvent = event.changedTouches[0], 177 | curX = targetEvent.clientX, 178 | curY = targetEvent.clientY, 179 | diffX = curX - _this.startX, 180 | diffY = curY - _this.startY; 181 | 182 | //判断垂直移动距离是否大于5 && 横向移动距离小于纵向移动距离 183 | if (Math.abs(diffY) > 5 && Math.abs(diffY) > Math.abs(diffX)) { 184 | if (diffY > 5 && scrollTop < _this.defaultConfig.offsetScrollTop) { 185 | //回调onPullDownRefresh 函数,即满足刷新条件 186 | _this.onPullDownRefresh(); 187 | } 188 | } 189 | }, _temp), _possibleConstructorReturn(_this, _ret); 190 | } 191 | //set props default values 192 | 193 | 194 | _createClass(ReactPullLoad, [{ 195 | key: 'componentDidMount', 196 | 197 | 198 | // container = null; 199 | 200 | value: function componentDidMount() { 201 | var _props = this.props; 202 | var isBlockContainer = _props.isBlockContainer; 203 | var offsetScrollTop = _props.offsetScrollTop; 204 | var downEnough = _props.downEnough; 205 | var distanceBottom = _props.distanceBottom; 206 | 207 | this.defaultConfig = { 208 | container: isBlockContainer ? (0, _reactDom.findDOMNode)(this) : document.body, 209 | offsetScrollTop: offsetScrollTop, 210 | downEnough: downEnough, 211 | distanceBottom: distanceBottom 212 | }; 213 | // console.info("downEnough = ", downEnough, this.defaultConfig.downEnough) 214 | /* 215 | As below reason handle touch event self ( widthout react defualt touch) 216 | Unable to preventDefault inside passive event listener due to target being treated as passive. See https://www.chromestatus.com/features/5093566007214080 217 | */ 218 | addEvent(this.refs.container, "touchstart", this.onTouchStart); 219 | addEvent(this.refs.container, "touchmove", this.onTouchMove); 220 | addEvent(this.refs.container, "touchend", this.onTouchEnd); 221 | } 222 | 223 | // 未考虑到 children 及其他 props 改变的情况 224 | // shouldComponentUpdate(nextProps, nextState) { 225 | // if(this.props.action === nextProps.action && this.state.pullHeight === nextState.pullHeight){ 226 | // //console.info("[ReactPullLoad] info new action is equal to old action",this.state.pullHeight,nextState.pullHeight); 227 | // return false 228 | // } else{ 229 | // return true 230 | // } 231 | // } 232 | 233 | }, { 234 | key: 'componentWillUnmount', 235 | value: function componentWillUnmount() { 236 | removeEvent(this.refs.container, "touchstart", this.onTouchStart); 237 | removeEvent(this.refs.container, "touchmove", this.onTouchMove); 238 | removeEvent(this.refs.container, "touchend", this.onTouchEnd); 239 | } 240 | }, { 241 | key: 'componentWillReceiveProps', 242 | value: function componentWillReceiveProps(nextProps) { 243 | var _this2 = this; 244 | 245 | if (nextProps.action === _constants.STATS.refreshed) { 246 | setTimeout(function () { 247 | _this2.props.handleAction(_constants.STATS.reset); 248 | }, 1000); 249 | } 250 | } 251 | 252 | // 拖拽的缓动公式 - easeOutSine 253 | 254 | }, { 255 | key: 'render', 256 | value: function render() { 257 | var _props2 = this.props; 258 | var children = _props2.children; 259 | var action = _props2.action; 260 | var handleAction = _props2.handleAction; 261 | var hasMore = _props2.hasMore; 262 | var className = _props2.className; 263 | var offsetScrollTop = _props2.offsetScrollTop; 264 | var downEnough = _props2.downEnough; 265 | var distanceBottom = _props2.distanceBottom; 266 | var isBlockContainer = _props2.isBlockContainer; 267 | var HeadNode = _props2.HeadNode; 268 | var FooterNode = _props2.FooterNode; 269 | 270 | var other = _objectWithoutProperties(_props2, ['children', 'action', 'handleAction', 'hasMore', 'className', 'offsetScrollTop', 'downEnough', 'distanceBottom', 'isBlockContainer', 'HeadNode', 'FooterNode']); 271 | 272 | var pullHeight = this.state.pullHeight; 273 | 274 | 275 | var msgStyle = pullHeight ? { 276 | WebkitTransform: 'translate3d(0, ' + pullHeight + 'px, 0)', 277 | transform: 'translate3d(0, ' + pullHeight + 'px, 0)' 278 | } : null; 279 | 280 | var boxClassName = className + ' pull-load state-' + action; 281 | 282 | return _react2.default.createElement( 283 | 'div', 284 | _extends({}, other, { 285 | className: boxClassName, 286 | ref: 'container' }), 287 | _react2.default.createElement( 288 | 'div', 289 | { className: 'pull-load-body', style: msgStyle }, 290 | _react2.default.createElement( 291 | 'div', 292 | { className: 'pull-load-head' }, 293 | _react2.default.createElement(HeadNode, { loaderState: action }) 294 | ), 295 | children, 296 | _react2.default.createElement( 297 | 'div', 298 | { className: 'pull-load-footer' }, 299 | _react2.default.createElement(FooterNode, { loaderState: action, hasMore: hasMore }) 300 | ) 301 | ) 302 | ); 303 | } 304 | }]); 305 | 306 | return ReactPullLoad; 307 | }(_react.Component); 308 | 309 | ReactPullLoad.propTypes = { 310 | action: _react.PropTypes.string.isRequired, //用于同步状态 311 | handleAction: _react.PropTypes.func.isRequired, //用于处理状态 312 | hasMore: _react.PropTypes.bool, //是否还有更多内容可加载 313 | offsetScrollTop: _react.PropTypes.number, //必须大于零,使触发刷新往下偏移,隐藏部分顶部内容 314 | downEnough: _react.PropTypes.number, //下拉满足刷新的距离 315 | distanceBottom: _react.PropTypes.number, //距离底部距离触发加载更多 316 | isBlockContainer: _react.PropTypes.bool, 317 | 318 | HeadNode: _react.PropTypes.any, //refresh message react dom 319 | FooterNode: _react.PropTypes.any }; 320 | ReactPullLoad.defaultProps = { 321 | hasMore: true, 322 | offsetScrollTop: 1, 323 | downEnough: 100, 324 | distanceBottom: 100, 325 | isBlockContainer: false, 326 | className: "", 327 | HeadNode: _HeadNode2.default, //refresh message react dom 328 | FooterNode: _FooterNode2.default }; 329 | exports.default = ReactPullLoad; -------------------------------------------------------------------------------- /lib/ReactPullLoad.less: -------------------------------------------------------------------------------- 1 | 2 | @transition-duration: .2s; 3 | 4 | //pull-load container 5 | .pull-load{ 6 | position: relative; 7 | overflow-y: scroll; 8 | -webkit-overflow-scrolling: touch; 9 | } 10 | //head load more msg and refreshing UI 11 | .pull-load-head{ 12 | position: absolute; 13 | transform: translate3d(0px, -100%, 0px); 14 | width: 100%; 15 | .state-refreshing &, 16 | .state-refreshed &{ 17 | position: relative; 18 | transform: none; 19 | } 20 | } 21 | //body container content 22 | .pull-load-body{ 23 | // transform: translate3d(0,0,0);// make over the msg-refreshed 24 | position: relative; 25 | .state-refreshing &{ 26 | // transform: translate3d(0,@height,0); 27 | transition: transform @transition-duration; 28 | } 29 | 30 | .state-refreshed &{ 31 | // handle resolve within 1s 32 | // animation: refreshed @transition-duration*5; 33 | } 34 | 35 | .state-reset &{ 36 | transition: transform @transition-duration; 37 | } 38 | } 39 | 40 | 41 | /* 42 | * HeadNode default UI 43 | */ 44 | @bg-dark: #EFEFF4; 45 | 46 | @height: 3rem; 47 | @fontSize: 12px; 48 | @fontColor: darken(@bg-dark, 40%);// state hint 49 | @btnColor: darken(@bg-dark, 60%);// load more 50 | 51 | @pullingMsg: '下拉刷新'; 52 | @pullingEnoughMsg: '松开刷新'; 53 | @refreshingMsg: '正在刷新...'; 54 | @refreshedMsg: '刷新成功'; 55 | @loadingMsg: '正在加载...'; 56 | @btnLoadMore: '加载更多'; 57 | @btnLoadNoMore: '没有更多'; 58 | 59 | .ui-loading(){ 60 | display: inline-block; 61 | vertical-align: middle; 62 | font-size: 1.5rem; 63 | width: 1em; 64 | height: 1em; 65 | border: 2px solid darken(@bg-dark, 30%); 66 | border-top-color: #fff; 67 | border-radius: 100%; 68 | animation: circle .8s infinite linear; 69 | } 70 | 71 | .pull-load-head-default{ 72 | text-align: center; font-size: @fontSize; line-height: @height; color: @fontColor; 73 | &:after{ 74 | .state-pulling &{ 75 | content: @pullingMsg 76 | } 77 | 78 | .state-pulling.enough &{ 79 | content: @pullingEnoughMsg; 80 | } 81 | 82 | .state-refreshing &{ 83 | content: @refreshingMsg; 84 | } 85 | .state-refreshed &{ 86 | content: @refreshedMsg; 87 | } 88 | } 89 | .state-pulling &{ 90 | opacity: 1; 91 | 92 | // arrow down icon 93 | i{ 94 | display: inline-block; 95 | font-size: 2em; 96 | margin-right: .6em; 97 | vertical-align: middle; 98 | height: 1em; 99 | border-left: 1px solid; 100 | position: relative; 101 | transition: transform .3s ease; 102 | 103 | &:before,&:after{ 104 | content: ''; 105 | position: absolute; 106 | font-size: .5em; 107 | width: 1em; 108 | bottom: 0px; 109 | border-top: 1px solid; 110 | } 111 | &:before{ 112 | right: 1px; 113 | transform: rotate(50deg); 114 | transform-origin: right; 115 | } 116 | &:after{ 117 | left: 0px; 118 | transform: rotate(-50deg); 119 | transform-origin: left; 120 | } 121 | } 122 | } 123 | .state-pulling.enough &{ 124 | // arrow up 125 | i{ 126 | transform: rotate(180deg); 127 | } 128 | } 129 | .state-refreshing &{ 130 | i{ 131 | margin-right: 10px; 132 | .ui-loading(); 133 | } 134 | } 135 | // 刷新成功提示消息 136 | .state-refreshed &{ 137 | opacity: 1; 138 | transition: opacity 1s; 139 | 140 | // √ icon 141 | i{ 142 | display: inline-block; 143 | box-sizing: content-box; 144 | vertical-align: middle; 145 | margin-right: 10px; 146 | font-size: 20px; 147 | height: 1em; 148 | width: 1em; 149 | border: 1px solid; 150 | border-radius: 100%; 151 | position: relative; 152 | 153 | &:before{ 154 | content: ''; 155 | position: absolute; 156 | top: 3px; 157 | left: 7px; 158 | height: 11px; 159 | width: 5px; 160 | border: solid; 161 | border-width: 0 1px 1px 0; 162 | transform: rotate(40deg); 163 | } 164 | } 165 | } 166 | } 167 | 168 | .pull-load-footer-default{ 169 | text-align: center; font-size: @fontSize; line-height: @height; color: @fontColor; 170 | &:after{ 171 | .state-loading &{ 172 | content: @btnLoadMore; 173 | } 174 | } 175 | &.nomore:after{ 176 | content: @btnLoadNoMore; 177 | } 178 | .state-loading &{ 179 | i{ 180 | margin-right: 10px; 181 | .ui-loading(); 182 | } 183 | } 184 | } 185 | // loading效果 186 | @keyframes circle { 187 | 100% { transform: rotate(360deg); } 188 | } -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | var STATS = exports.STATS = { 7 | init: '', 8 | pulling: 'pulling', 9 | enough: 'pulling enough', 10 | refreshing: 'refreshing', 11 | refreshed: 'refreshed', 12 | reset: 'reset', 13 | 14 | loading: 'loading' // loading more 15 | }; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = exports.STATS = undefined; 7 | 8 | var _constants = require('./constants'); 9 | 10 | Object.defineProperty(exports, 'STATS', { 11 | enumerable: true, 12 | get: function get() { 13 | return _constants.STATS; 14 | } 15 | }); 16 | 17 | var _ReactPullLoad = require('./ReactPullLoad'); 18 | 19 | var _ReactPullLoad2 = _interopRequireDefault(_ReactPullLoad); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | exports.default = _ReactPullLoad2.default; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-pullload", 3 | "version": "1.2.0", 4 | "description": "React compopnent pull down refresh and pull up load more", 5 | "main": "./dist/index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --config webpack.config.js", 8 | "example": "rm -rf ./demo/* & NODE_ENV=development webpack --config webpack.config.example.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/react-ld/react-pullLoad.git" 14 | }, 15 | "keywords": [ 16 | "react", 17 | "refresh", 18 | "component", 19 | "loadmore" 20 | ], 21 | "author": "lidianhao123", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/react-ld/react-pullLoad/issues" 25 | }, 26 | "files": [ 27 | "dist", 28 | "example", 29 | "src" 30 | ], 31 | "homepage": "https://github.com/react-ld/react-pullLoad#readme", 32 | "devDependencies": { 33 | "autoprefixer": "^6.5.1", 34 | "babel-cli": "^6.16.0", 35 | "babel-core": "^6.17.0", 36 | "babel-loader": "^6.2.5", 37 | "babel-polyfill": "^6.16.0", 38 | "babel-preset-es2015": "^6.16.0", 39 | "babel-preset-react": "^6.16.0", 40 | "babel-preset-stage-1": "^6.16.0", 41 | "css-loader": "^0.25.0", 42 | "gulp": "^3.9.1", 43 | "gulp-babel": "^7.0.0", 44 | "gulp-clean": "^0.3.2", 45 | "gulp-gh-pages": "git@github.com:tekd/gulp-gh-pages.git#update-dependency", 46 | "gulp-less": "^3.3.2", 47 | "gulp-util": "^3.0.8", 48 | "html-webpack-plugin": "^2.22.0", 49 | "less": "^2.7.1", 50 | "less-loader": "^2.2.3", 51 | "postcss": "^5.2.4", 52 | "postcss-loader": "^0.13.0", 53 | "react": "^16.0.0", 54 | "react-dom": "^16.0.0", 55 | "react-hot-loader": "^3.0.0-beta.6", 56 | "style-loader": "^0.13.1", 57 | "webpack": "^2.6.1", 58 | "webpack-dev-server": "^2.4.5" 59 | }, 60 | "dependencies": { 61 | "prop-types": "^15.6.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ file, options, env }) => ({ 2 | // parser: file.extname === '.sss' ? 'sugarss' : false, 3 | // plugins: { 4 | // 'postcss-import': { root: file.dirname }, 5 | // 'postcss-cssnext': options.cssnext ? options.cssnext : false, 6 | // 'autoprefixer': env == 'production' ? options.autoprefixer : false, 7 | // 'cssnano': env === 'production' ? options.cssnano : false 8 | // } 9 | plugins: [ require('autoprefixer')({ browsers: ["Android >= 4", "iOS >= 7"]}) ] 10 | }) -------------------------------------------------------------------------------- /src/FooterNode.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { PureComponent } from 'react' 3 | import PropTypes from 'prop-types'; 4 | import { STATS } from './constants' 5 | 6 | export default class FooterNode extends PureComponent{ 7 | 8 | static propTypes = { 9 | loaderState: PropTypes.string.isRequired, 10 | hasMore: PropTypes.bool.isRequired 11 | }; 12 | 13 | static defaultProps = { 14 | loaderState: STATS.init, 15 | hasMore: true 16 | }; 17 | 18 | render(){ 19 | const { 20 | loaderState, 21 | hasMore 22 | } = this.props 23 | 24 | let className = `pull-load-footer-default ${hasMore? "" : "nomore"}` 25 | 26 | return( 27 |
28 | { 29 | loaderState === STATS.loading ? : "" 30 | } 31 |
32 | ) 33 | } 34 | } -------------------------------------------------------------------------------- /src/HeadNode.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { PureComponent } from 'react' 3 | import PropTypes from 'prop-types'; 4 | import { STATS } from './constants' 5 | 6 | export default class HeadNode extends PureComponent{ 7 | 8 | static propTypes = { 9 | loaderState: PropTypes.string.isRequired, 10 | }; 11 | 12 | static defaultProps = { 13 | loaderState: STATS.init, 14 | }; 15 | 16 | render(){ 17 | const { 18 | loaderState 19 | } = this.props 20 | 21 | return( 22 |
23 | 24 |
25 | ) 26 | } 27 | } -------------------------------------------------------------------------------- /src/ReactPullLoad.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import PropTypes from 'prop-types'; 4 | import { findDOMNode } from 'react-dom' 5 | import { STATS } from './constants' 6 | import HeadNode from './HeadNode' 7 | import FooterNode from './FooterNode' 8 | 9 | function addEvent(obj, type, fn) { 10 | if (obj.attachEvent) { 11 | obj['e' + type + fn] = fn; 12 | obj[type + fn] = function () { obj['e' + type + fn](window.event); } 13 | obj.attachEvent('on' + type, obj[type + fn]); 14 | } else 15 | obj.addEventListener(type, fn, false, {passive: false}); 16 | } 17 | function removeEvent(obj, type, fn) { 18 | if (obj.detachEvent) { 19 | obj.detachEvent('on' + type, obj[type + fn]); 20 | obj[type + fn] = null; 21 | } else 22 | obj.removeEventListener(type, fn, false); 23 | } 24 | 25 | export default class ReactPullLoad extends Component { 26 | static propTypes = { 27 | action: PropTypes.string.isRequired, //用于同步状态 28 | handleAction: PropTypes.func.isRequired, //用于处理状态 29 | hasMore: PropTypes.bool, //是否还有更多内容可加载 30 | offsetScrollTop: PropTypes.number,//必须大于零,使触发刷新往下偏移,隐藏部分顶部内容 31 | downEnough: PropTypes.number, //下拉满足刷新的距离 32 | distanceBottom: PropTypes.number, //距离底部距离触发加载更多 33 | isBlockContainer: PropTypes.bool, 34 | 35 | HeadNode: PropTypes.any, //refresh message react dom 36 | FooterNode: PropTypes.any, //refresh loading react dom 37 | }; 38 | //set props default values 39 | static defaultProps = { 40 | hasMore: true, 41 | offsetScrollTop: 1, 42 | downEnough: 100, 43 | distanceBottom: 100, 44 | isBlockContainer: false, 45 | className: "", 46 | HeadNode: HeadNode, //refresh message react dom 47 | FooterNode: FooterNode, //refresh loading react dom 48 | }; 49 | 50 | state = { 51 | pullHeight: 0 52 | }; 53 | 54 | // container = null; 55 | 56 | componentDidMount() { 57 | const {isBlockContainer, offsetScrollTop, downEnough, distanceBottom} = this.props 58 | this.defaultConfig = { 59 | container: isBlockContainer ? findDOMNode(this) : document.body, 60 | offsetScrollTop: offsetScrollTop, 61 | downEnough: downEnough, 62 | distanceBottom: distanceBottom 63 | }; 64 | // console.info("downEnough = ", downEnough, this.defaultConfig.downEnough) 65 | /* 66 | As below reason handle touch event self ( widthout react defualt touch) 67 | Unable to preventDefault inside passive event listener due to target being treated as passive. See https://www.chromestatus.com/features/5093566007214080 68 | */ 69 | addEvent(this.refs.container, "touchstart", this.onTouchStart) 70 | addEvent(this.refs.container, "touchmove", this.onTouchMove) 71 | addEvent(this.refs.container, "touchend", this.onTouchEnd) 72 | } 73 | 74 | // 未考虑到 children 及其他 props 改变的情况 75 | // shouldComponentUpdate(nextProps, nextState) { 76 | // if(this.props.action === nextProps.action && this.state.pullHeight === nextState.pullHeight){ 77 | // //console.info("[ReactPullLoad] info new action is equal to old action",this.state.pullHeight,nextState.pullHeight); 78 | // return false 79 | // } else{ 80 | // return true 81 | // } 82 | // } 83 | 84 | componentWillUnmount() { 85 | removeEvent(this.refs.container, "touchstart", this.onTouchStart) 86 | removeEvent(this.refs.container, "touchmove", this.onTouchMove) 87 | removeEvent(this.refs.container, "touchend", this.onTouchEnd) 88 | } 89 | 90 | componentWillReceiveProps(nextProps) { 91 | if(nextProps.action === STATS.refreshed){ 92 | setTimeout(()=>{ 93 | this.props.handleAction(STATS.reset) 94 | },1000) 95 | } 96 | } 97 | 98 | getScrollTop = ()=>{ 99 | if(this.defaultConfig.container){ 100 | if(this.defaultConfig.container === document.body){ 101 | return document.documentElement.scrollTop || document.body.scrollTop; 102 | } 103 | return this.defaultConfig.container.scrollTop; 104 | } else{ 105 | return 0; 106 | } 107 | } 108 | 109 | setScrollTop = (value)=>{ 110 | if(this.defaultConfig.container){ 111 | let scrollH = this.defaultConfig.container.scrollHeight; 112 | if(value < 0){ value = 0} 113 | if(value > scrollH){ value = scrollH} 114 | return this.defaultConfig.container.scrollTop = value; 115 | } else{ 116 | return 0; 117 | } 118 | } 119 | 120 | // 拖拽的缓动公式 - easeOutSine 121 | easing = (distance) => { 122 | // t: current time, b: begInnIng value, c: change In value, d: duration 123 | var t = distance; 124 | var b = 0; 125 | var d = screen.availHeight; // 允许拖拽的最大距离 126 | var c = d / 2.5; // 提示标签最大有效拖拽距离 127 | 128 | return c * Math.sin(t / d * (Math.PI / 2)) + b; 129 | } 130 | 131 | canRefresh = () => { 132 | return [STATS.refreshing, STATS.loading].indexOf(this.props.action) < 0; 133 | } 134 | 135 | onPullDownMove = (data) => { 136 | if(!this.canRefresh())return false; 137 | 138 | let loaderState, diff = data[0].touchMoveY - data[0].touchStartY; 139 | if (diff < 0) { 140 | diff = 0; 141 | } 142 | diff = this.easing(diff); 143 | if (diff > this.defaultConfig.downEnough) { 144 | loaderState = STATS.enough 145 | } else { 146 | loaderState = STATS.pulling 147 | } 148 | this.setState({ 149 | pullHeight: diff, 150 | }) 151 | this.props.handleAction(loaderState) 152 | } 153 | 154 | onPullDownRefresh = () => { 155 | if(!this.canRefresh())return false; 156 | 157 | if (this.props.action === STATS.pulling) { 158 | this.setState({pullHeight: 0}) 159 | this.props.handleAction(STATS.reset) 160 | } else { 161 | this.setState({ 162 | pullHeight: 0, 163 | }) 164 | this.props.handleAction(STATS.refreshing) 165 | } 166 | } 167 | 168 | onPullUpMove = (data) => { 169 | if(!this.canRefresh())return false; 170 | 171 | // const { hasMore, onLoadMore} = this.props 172 | // if (this.props.hasMore) { 173 | this.setState({ 174 | pullHeight: 0, 175 | }) 176 | this.props.handleAction(STATS.loading) 177 | // } 178 | } 179 | 180 | onTouchStart = (event) => { 181 | var targetEvent = event.changedTouches[0]; 182 | this.startX = targetEvent.clientX; 183 | this.startY = targetEvent.clientY; 184 | } 185 | 186 | onTouchMove = (event) => { 187 | let scrollTop = this.getScrollTop(), 188 | scrollH = this.defaultConfig.container.scrollHeight, 189 | conH = this.defaultConfig.container === document.body ? document.documentElement.clientHeight : this.defaultConfig.container.offsetHeight, 190 | targetEvent = event.changedTouches[0], 191 | curX = targetEvent.clientX, 192 | curY = targetEvent.clientY, 193 | diffX = curX - this.startX, 194 | diffY = curY - this.startY; 195 | 196 | //判断垂直移动距离是否大于5 && 横向移动距离小于纵向移动距离 197 | if (Math.abs(diffY) > 5 && Math.abs(diffY) > Math.abs(diffX)) { 198 | //滚动距离小于设定值 &&回调onPullDownMove 函数,并且回传位置值 199 | if (diffY > 5 && scrollTop < this.defaultConfig.offsetScrollTop) { 200 | //阻止执行浏览器默认动作 201 | event.preventDefault(); 202 | this.onPullDownMove([{ 203 | touchStartY: this.startY, 204 | touchMoveY: curY 205 | }]); 206 | } //滚动距离距离底部小于设定值 207 | else if (diffY < 0 && (scrollH - scrollTop - conH) < this.defaultConfig.distanceBottom) { 208 | //阻止执行浏览器默认动作 209 | // event.preventDefault(); 210 | this.onPullUpMove([{ 211 | touchStartY: this.startY, 212 | touchMoveY: curY 213 | }]); 214 | } 215 | } 216 | } 217 | 218 | onTouchEnd = (event) => { 219 | let scrollTop = this.getScrollTop(), 220 | targetEvent = event.changedTouches[0], 221 | curX = targetEvent.clientX, 222 | curY = targetEvent.clientY, 223 | diffX = curX - this.startX, 224 | diffY = curY - this.startY; 225 | 226 | //判断垂直移动距离是否大于5 && 横向移动距离小于纵向移动距离 227 | if (Math.abs(diffY) > 5 && Math.abs(diffY) > Math.abs(diffX)) { 228 | if (diffY > 5 && scrollTop < this.defaultConfig.offsetScrollTop) { 229 | //回调onPullDownRefresh 函数,即满足刷新条件 230 | this.onPullDownRefresh(); 231 | } 232 | } 233 | } 234 | 235 | render() { 236 | const { 237 | children, 238 | action, 239 | handleAction, 240 | hasMore, 241 | className, 242 | offsetScrollTop, 243 | downEnough, 244 | distanceBottom, 245 | isBlockContainer, 246 | HeadNode, 247 | FooterNode, 248 | ...other 249 | } = this.props 250 | 251 | const { pullHeight } = this.state 252 | 253 | const msgStyle = pullHeight ? { 254 | WebkitTransform: `translate3d(0, ${pullHeight}px, 0)`, 255 | transform: `translate3d(0, ${pullHeight}px, 0)` 256 | } : null; 257 | 258 | const boxClassName = `${className} pull-load state-${action}`; 259 | 260 | return ( 261 |
264 |
265 |
266 | 267 |
268 | { children } 269 |
270 | 271 |
272 |
273 |
274 | ) 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/ReactPullLoad.less: -------------------------------------------------------------------------------- 1 | 2 | @transition-duration: .2s; 3 | 4 | //pull-load container 5 | .pull-load{ 6 | position: relative; 7 | overflow-y: scroll; 8 | -webkit-overflow-scrolling: touch; 9 | } 10 | //head load more msg and refreshing UI 11 | .pull-load-head{ 12 | position: absolute; 13 | transform: translate3d(0px, -100%, 0px); 14 | width: 100%; 15 | .state-refreshing &, 16 | .state-refreshed &{ 17 | position: relative; 18 | transform: none; 19 | } 20 | } 21 | //body container content 22 | .pull-load-body{ 23 | // transform: translate3d(0,0,0);// make over the msg-refreshed 24 | position: relative; 25 | .state-refreshing &{ 26 | // transform: translate3d(0,@height,0); 27 | transition: transform @transition-duration; 28 | } 29 | 30 | .state-refreshed &{ 31 | // handle resolve within 1s 32 | // animation: refreshed @transition-duration*5; 33 | } 34 | 35 | .state-reset &{ 36 | transition: transform @transition-duration; 37 | } 38 | } 39 | 40 | 41 | /* 42 | * HeadNode default UI 43 | */ 44 | @bg-dark: #EFEFF4; 45 | 46 | @height: 3rem; 47 | @fontSize: 12px; 48 | @fontColor: darken(@bg-dark, 40%);// state hint 49 | @btnColor: darken(@bg-dark, 60%);// load more 50 | 51 | @pullingMsg: '下拉刷新'; 52 | @pullingEnoughMsg: '松开刷新'; 53 | @refreshingMsg: '正在刷新...'; 54 | @refreshedMsg: '刷新成功'; 55 | @loadingMsg: '正在加载...'; 56 | @btnLoadMore: '加载更多'; 57 | @btnLoadNoMore: '没有更多'; 58 | 59 | .ui-loading(){ 60 | display: inline-block; 61 | vertical-align: middle; 62 | font-size: 1.5rem; 63 | width: 1em; 64 | height: 1em; 65 | border: 2px solid darken(@bg-dark, 30%); 66 | border-top-color: #fff; 67 | border-radius: 100%; 68 | animation: circle .8s infinite linear; 69 | } 70 | 71 | .pull-load-head-default{ 72 | text-align: center; font-size: @fontSize; line-height: @height; color: @fontColor; 73 | &:after{ 74 | .state-pulling &{ 75 | content: @pullingMsg 76 | } 77 | 78 | .state-pulling.enough &{ 79 | content: @pullingEnoughMsg; 80 | } 81 | 82 | .state-refreshing &{ 83 | content: @refreshingMsg; 84 | } 85 | .state-refreshed &{ 86 | content: @refreshedMsg; 87 | } 88 | } 89 | .state-pulling &{ 90 | opacity: 1; 91 | 92 | // arrow down icon 93 | i{ 94 | display: inline-block; 95 | font-size: 2em; 96 | margin-right: .6em; 97 | vertical-align: middle; 98 | height: 1em; 99 | border-left: 1px solid; 100 | position: relative; 101 | transition: transform .3s ease; 102 | 103 | &:before,&:after{ 104 | content: ''; 105 | position: absolute; 106 | font-size: .5em; 107 | width: 1em; 108 | bottom: 0px; 109 | border-top: 1px solid; 110 | } 111 | &:before{ 112 | right: 1px; 113 | transform: rotate(50deg); 114 | transform-origin: right; 115 | } 116 | &:after{ 117 | left: 0px; 118 | transform: rotate(-50deg); 119 | transform-origin: left; 120 | } 121 | } 122 | } 123 | .state-pulling.enough &{ 124 | // arrow up 125 | i{ 126 | transform: rotate(180deg); 127 | } 128 | } 129 | .state-refreshing &{ 130 | i{ 131 | margin-right: 10px; 132 | .ui-loading(); 133 | } 134 | } 135 | // 刷新成功提示消息 136 | .state-refreshed &{ 137 | opacity: 1; 138 | transition: opacity 1s; 139 | 140 | // √ icon 141 | i{ 142 | display: inline-block; 143 | box-sizing: content-box; 144 | vertical-align: middle; 145 | margin-right: 10px; 146 | font-size: 20px; 147 | height: 1em; 148 | width: 1em; 149 | border: 1px solid; 150 | border-radius: 100%; 151 | position: relative; 152 | 153 | &:before{ 154 | content: ''; 155 | position: absolute; 156 | top: 3px; 157 | left: 7px; 158 | height: 11px; 159 | width: 5px; 160 | border: solid; 161 | border-width: 0 1px 1px 0; 162 | transform: rotate(40deg); 163 | } 164 | } 165 | } 166 | } 167 | 168 | .pull-load-footer-default{ 169 | text-align: center; font-size: @fontSize; line-height: @height; color: @fontColor; 170 | &:after{ 171 | .state-loading &{ 172 | content: @btnLoadMore; 173 | } 174 | } 175 | &.nomore:after{ 176 | content: @btnLoadNoMore; 177 | } 178 | .state-loading &{ 179 | i{ 180 | margin-right: 10px; 181 | .ui-loading(); 182 | } 183 | } 184 | } 185 | // loading效果 186 | @keyframes circle { 187 | 100% { transform: rotate(360deg); } 188 | } -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | 2 | export const STATS = { 3 | init: '', 4 | pulling: 'pulling', 5 | enough: 'pulling enough', 6 | refreshing: 'refreshing', 7 | refreshed: 'refreshed', 8 | reset: 'reset', 9 | 10 | loading: 'loading' // loading more 11 | }; -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | declare enum STATS { 3 | init = "", 4 | pulling = "pulling", 5 | enough = "pulling enough", 6 | refreshing = "refreshing", 7 | refreshed = "refreshed", 8 | reset = "reset", 9 | 10 | loading = "loading" // loading more 11 | } 12 | 13 | export interface PullLoadProps { 14 | action: STATS; //用于同步状态 15 | handleAction: (action: STATS) => void; //用于处理状态 16 | hasMore: boolean; //是否还有更多内容可加载 17 | offsetScrollTop?: number; //必须大于零,使触发刷新往下偏移,隐藏部分顶部内容 18 | downEnough?: number; //下拉满足刷新的距离 19 | distanceBottom?: number; //距离底部距离触发加载更多 20 | isBlockContainer?: boolean; 21 | 22 | HeadNode?: React.ReactNode | string; //refresh message react dom 23 | FooterNode?: React.ReactNode | string; //refresh loading react dom 24 | children: React.ReactChild; // 子组件 25 | } 26 | export default class ReactPullLoad extends React.Component< 27 | PullLoadProps, 28 | any 29 | > {} 30 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | // import { STATS as _STATS } from 'constants' 3 | // export const STATS = _STATS 4 | // export default ReactPullLoad 5 | export { STATS } from './constants' 6 | export default from './ReactPullLoad' -------------------------------------------------------------------------------- /webpack.config.example.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var webpack = require("webpack"); 3 | var HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | 5 | module.exports = { 6 | context: path.resolve(__dirname, "example"), // string(绝对路径!) 7 | devtool: "eval", 8 | cache: true, 9 | entry: { 10 | bundle1: ["babel-polyfill", "./App1.jsx"], 11 | bundle2: ["babel-polyfill", "./App2.jsx"], 12 | bundle3: ["babel-polyfill", "./App3.jsx"], 13 | bundle4: ["babel-polyfill", "./App4.jsx"] 14 | }, 15 | output: { 16 | path: path.join(__dirname, "demo/"), 17 | filename: "[name].js" 18 | }, 19 | plugins: [ 20 | // new webpack.optimize.OccurenceOrderPlugin(), 21 | new webpack.DefinePlugin({ 22 | "process.env": { 23 | NODE_ENV: JSON.stringify("production") 24 | } 25 | }), 26 | new webpack.NamedModulesPlugin(), 27 | // 当模块热替换(HMR)时在浏览器控制台输出对用户更友好的模块名字信息 28 | new webpack.optimize.UglifyJsPlugin({ 29 | sourceMap: true, 30 | compress: { 31 | warnings: true 32 | } 33 | }) 34 | // new HtmlWebpackPlugin({ template: 'index.html' }) 35 | ], 36 | resolve: { 37 | extensions: [".js", ".jsx"], 38 | modules: ["node_modules", path.resolve(__dirname, "src")] 39 | }, 40 | 41 | module: { 42 | rules: [ 43 | { 44 | test: /\.(js|jsx)$/, 45 | loader: "babel-loader", 46 | exclude: /node_modules/, 47 | include: __dirname, 48 | options: { 49 | presets: [["es2015", { modules: false }], "stage-1", "react"] 50 | } 51 | }, 52 | { 53 | test: /\.css$/, 54 | use: [ 55 | "style-loader", 56 | "css-loader", 57 | { loader: "postcss-loader", options: { config: { path: "./postcss.config.js" } } } 58 | ] 59 | }, 60 | { 61 | test: /\.less/, 62 | use: [ 63 | "style-loader", 64 | "css-loader", 65 | { loader: "postcss-loader", options: { config: { path: "./postcss.config.js" } } }, 66 | "less-loader" 67 | ] 68 | }, 69 | { 70 | test: /\.(gif|jpg|png|woff|svg|eot|ttf)$/, 71 | use: [{ loader: "file-loader" }] 72 | } 73 | ] 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | var path = require("path"); 4 | var port = 3010; 5 | var demoNum = 3; 6 | 7 | module.exports = { 8 | context: path.resolve(__dirname, "example"), // string(绝对路径!) 9 | devtool: "eval", 10 | cache: true, 11 | entry: [ 12 | "react-hot-loader/patch", 13 | // 开启 React 代码的模块热替换(HMR) 14 | "webpack-dev-server/client?http://0.0.0.0:" + port, 15 | "webpack/hot/only-dev-server", 16 | "./App"+demoNum+".jsx" 17 | ], 18 | plugins: [ 19 | new webpack.HotModuleReplacementPlugin(), 20 | new webpack.NamedModulesPlugin(), 21 | // 当模块热替换(HMR)时在浏览器控制台输出对用户更友好的模块名字信息 22 | new webpack.optimize.UglifyJsPlugin({ 23 | sourceMap: true, 24 | compress: { 25 | warnings: true 26 | } 27 | }), 28 | new HtmlWebpackPlugin({ 29 | title: "Custom template", 30 | template: "./index"+demoNum+".html", // Load a custom template (ejs by default see the FAQ for details) 31 | hash: true, 32 | filename: "./index.html" 33 | }) 34 | ], 35 | resolve: { 36 | modules: ["node_modules", path.resolve(__dirname, "src")], 37 | extensions: [".js", ".jsx"] 38 | }, 39 | devServer: { 40 | hot: true, 41 | // 开启服务器的模块热替换(HMR) 42 | host: "0.0.0.0", 43 | port: port 44 | }, 45 | module: { 46 | rules: [ 47 | { 48 | test: /\.(js|jsx)$/, 49 | loader: "babel-loader", 50 | exclude: /node_modules/, 51 | include: __dirname, 52 | options: { 53 | presets: [["es2015", { modules: false }], "stage-1", "react"], 54 | plugins: [ 55 | "react-hot-loader/babel" 56 | // 开启 React 代码的模块热替换(HMR) 57 | ] 58 | } 59 | }, 60 | { 61 | test: /\.css$/, 62 | use: [ 63 | "style-loader", 64 | "css-loader", 65 | { loader: "postcss-loader", options: { config: { path: "./postcss.config.js" } } } 66 | ] 67 | }, 68 | { 69 | test: /\.less/, 70 | use: [ 71 | "style-loader", 72 | "css-loader", 73 | { loader: "postcss-loader", options: { config: { path: "./postcss.config.js" } } }, 74 | "less-loader" 75 | ] 76 | }, 77 | { 78 | test: /\.(gif|jpg|png|woff|svg|eot|ttf)$/, 79 | use: [{ loader: "file-loader" }] 80 | } 81 | ] 82 | } 83 | }; 84 | --------------------------------------------------------------------------------