├── .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 |
159 |
160 |
161 | {data.map((str, index) => {
162 | return (
163 | -
164 |
165 |
166 | );
167 | })}
168 |
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 |
151 |
152 |
153 | {data.map((str, index) => {
154 | return (
155 | -
156 |
157 |
158 | );
159 | })}
160 |
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 |
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 |
--------------------------------------------------------------------------------