Repository: react-ld/react-pullLoad Branch: master Commit: 62ba824bac1b Files: 32 Total size: 78.4 KB Directory structure: gitextract_gef5ggiu/ ├── .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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": ["es2015", "stage-1", "react"] } ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* package-lock.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Typescript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env demo deploy.config.json dist .publish ================================================ FILE: README-cn.md ================================================ # [English](./README.md) # [react-pullLoad](https://github.com/react-ld/react-pullLoad) React 版本的 [pullLoad](https://github.com/lidianhao123/pullLoad) 下拉更新 上拉加载更多 组件 [pullLoad](https://github.com/lidianhao123/pullLoad) 非 react 版本,支持 require.js 模块化调用 #### 示例 [demo1](https://react-ld.github.io/react-pullLoad/index1.html) ReactPullLoad 根节点 DOM 作为容器 [demo2](https://react-ld.github.io/react-pullLoad/index2.html) ReactPullLoad 根节点 DOM 作为容器 [demo3](https://react-ld.github.io/react-pullLoad/index3.html) document.body 作为容器 且自定义刷新和加载更多 UI 组件 [demo4](https://react-ld.github.io/react-pullLoad/index4.html) 禁用下拉刷新功能 # 当前版本 1.2.0 支持 Typescript # 简介 1. 只依赖 react/react-dom 2. 样式使用 less 编写 3. 支持 body 或者组件根 DOM 固定高度作为外部容器 contianer(即可视区域大小) 4. 触摸事件绑定在内容块 content(即高度为 auto 的 DOM ) 5. 纯 React 组件方式开发的 6. 支持自定义刷新和加载更多 UI 组件 7. 支持代码动态调起刷新和加载更多(组件将展示刷新和加载更多样式) 8. **只支持移动触摸设备** # 功能点 1. 下拉距离大于阈值触发刷新动作 2. 滚动到距底部距离小于阈值加载更多 3. 支持自定义刷新和加载更多 UI 组件 # 使用说明 ```sh npm install --save react-pullload ``` ```js import ReactPullLoad, { STATS } from "react-pullload"; import "node_modules/react-pullload/dist/ReactPullLoad.css"; export class App extends Component { constructor() { super(); this.state = { hasMore: true, data: cData, action: STATS.init, index: loadMoreLimitNum //loading more test time limit }; } handleAction = action => { console.info(action, this.state.action, action === this.state.action); //new action must do not equel to old action if (action === this.state.action) { return false; } if (action === STATS.refreshing) { //刷新 this.handRefreshing(); } else if (action === STATS.loading) { //加载更多 this.handLoadMore(); } else { //DO NOT modify below code this.setState({ action: action }); } }; handRefreshing = () => { if (STATS.refreshing === this.state.action) { return false; } setTimeout(() => { //refreshing complete this.setState({ data: cData, hasMore: true, action: STATS.refreshed, index: loadMoreLimitNum }); }, 3000); this.setState({ action: STATS.refreshing }); }; handLoadMore = () => { if (STATS.loading === this.state.action) { return false; } //无更多内容则不执行后面逻辑 if (!this.state.hasMore) { return; } setTimeout(() => { if (this.state.index === 0) { this.setState({ action: STATS.reset, hasMore: false }); } else { this.setState({ data: [...this.state.data, cData[0], cData[0]], action: STATS.reset, index: this.state.index - 1 }); } }, 3000); this.setState({ action: STATS.loading }); }; render() { const { data, hasMore } = this.state; const fixHeaderStyle = { position: "fixed", width: "100%", height: "50px", color: "#fff", lineHeight: "50px", backgroundColor: "#e24f37", left: 0, top: 0, textAlign: "center", zIndex: 1 }; return (
fixed header
); } } ``` # 参数说明: | 参数 | 说明 | 类型 | 默认值 | 备注 | | ---------------- | --------------------------------------------- | ------ | ------------------------------------------------ | --------------------- | | action | 用于同步状态 | string | | isRequired | | handleAction | 用于处理状态 | func | | isRequired | | hasMore | 是否还有更多内容可加载 | bool | false | | | downEnough | 下拉距离是否满足要求 | num | 100 | | | distanceBottom | 距离底部距离触发加载更多 | num | 100 | | | isBlockContainer | 是否开启使用组件根 DOM 作为外部容器 contianer | bool | false | | | HeadNode | 自定义顶部刷新 UI 组件 | any | [ReactPullLoad HeadNode](./src/HeadNode.jsx) | 必须是一个 React 组件 | | FooterNode | 自定义底部加载更多 UI 组件 | any | [ReactPullLoad FooterNode](./src/FooterNode.jsx) | 必须是一个 React 组件 | 另外 ReactPullLoad 组件支持根属性扩展 例如: className\style 等等 # STATS list | 属性 | 值 | 根节点 className | 说明 | | ---------- | ---------------- | -------------------- | -------------------- | | init | '' | | 组件初始状态 | | pulling | 'pulling' | state-pulling | 下拉状态 | | enough | 'pulling enough' | state-pulling.enough | 下拉并且已经满足阈值 | | refreshing | 'refreshing' | state-refreshing | 刷新中(加载数据中) | | refreshed | 'refreshed' | state-refreshed | 完成刷新动作 | | reset | 'reset' | state-reset | 恢复默认状态 | | loading | 'loading' | state-loading | 加载中 | init/reset -> pulling -> enough -> refreshing -> refreshed -> reset init/reset -> pulling -> reset init/reset -> loading -> reset # 自定义刷新及加载组件 请参考默认刷新及加载组件源码(通过 css 根节点不同 className 设置对应 UI 样式来实现) [ReactPullLoad HeadNode](./src/HeadNode.jsx) [ReactPullLoad FooterNode](./src/FooterNode.jsx) 或参考 demo3 中的实现方式在组件内容通过获取的 loaderState 与 STATS 不同状态对比来现实 [demo3](https://react-ld.github.io/react-pullLoad/index3.html) # License MIT ================================================ FILE: README.md ================================================ # [中文](./README-cn.md) # [react-pullLoad](https://github.com/react-ld/react-pullLoad) Refreshing and Loading more component for react. [pullLoad](https://github.com/lidianhao123/pullLoad) is another refreshing and loading more lib without react, support require.js to load lib. #### examples [demo1](https://react-ld.github.io/react-pullLoad/index1.html) use ReactPullLoad root DOM as container [demo2](https://react-ld.github.io/react-pullLoad/index2.html) use ReactPullLoad root DOM as container [demo3](https://react-ld.github.io/react-pullLoad/index3.html) use document.body as container, and config UI component (HeadNode and FooterNode). [demo4](https://react-ld.github.io/react-pullLoad/index4.html) forbidden pull refresh # version 1.2.0 Support Typescript # Description 1. Only depend on react/react-dom, without any other package. 2. Use less. 3. Support body or root Dom as container. 4. Bind touch event on component root Dom. 5. It.s develop as Pure react component. 6. Support config UI component (HeadNode and FooterNode). 7. Can apply refreshing or loading through modify STATE. 8. **Only support mobile device** # How to use ```sh npm install --save react-pullload ``` ```js import ReactPullLoad, { STATS } from "react-pullload"; import "node_modules/react-pullload/dist/ReactPullLoad.css"; export class App extends Component { constructor() { super(); this.state = { hasMore: true, data: cData, action: STATS.init, index: loadMoreLimitNum //loading more test time limit }; } handleAction = action => { console.info(action, this.state.action, action === this.state.action); //new action must do not equel to old action if (action === this.state.action) { return false; } if (action === STATS.refreshing) { this.handRefreshing(); } else if (action === STATS.loading) { this.handLoadMore(); } else { //DO NOT modify below code this.setState({ action: action }); } }; handRefreshing = () => { if (STATS.refreshing === this.state.action) { return false; } setTimeout(() => { //refreshing complete this.setState({ data: cData, hasMore: true, action: STATS.refreshed, index: loadMoreLimitNum }); }, 3000); this.setState({ action: STATS.refreshing }); }; handLoadMore = () => { if (STATS.loading === this.state.action) { return false; } //无更多内容则不执行后面逻辑 if (!this.state.hasMore) { return; } setTimeout(() => { if (this.state.index === 0) { this.setState({ action: STATS.reset, hasMore: false }); } else { this.setState({ data: [...this.state.data, cData[0], cData[0]], action: STATS.reset, index: this.state.index - 1 }); } }, 3000); this.setState({ action: STATS.loading }); }; render() { const { data, hasMore } = this.state; const fixHeaderStyle = { position: "fixed", width: "100%", height: "50px", color: "#fff", lineHeight: "50px", backgroundColor: "#e24f37", left: 0, top: 0, textAlign: "center", zIndex: 1 }; return (
fixed header
); } } ``` # API: | Property | Description | Type | default | Remarks | | ---------------- | ------------------------------------------- | ------ | ------------------------------------------------ | ------------------------- | | action | sync component status | string | | isRequired | | handleAction | handle status | func | | isRequired | | hasMore | flag for are there any more content to load | bool | false | | | downEnough | how long distance is enough to refreshing | num | 100 | use px as unit | | distanceBottom | current position is apart from bottom | num | 100 | use px as unit | | isBlockContainer | set root dom as container | bool | false | | | HeadNode | custom header UI compoent | any | [ReactPullLoad HeadNode](./src/HeadNode.jsx) | must be a react component | | FooterNode | custom footer UI compoent | any | [ReactPullLoad FooterNode](./src/FooterNode.jsx) | must be a react component | Remarks: ReactPullLoad support set root dom className and style. # STATS list | Property | Value | root className | explain | | ---------- | ---------------- | -------------------- | ---------------------------- | | init | '' | | component initial status | | pulling | 'pulling' | state-pulling | pull status | | enough | 'pulling enough' | state-pulling.enough | pull down enough status | | refreshing | 'refreshing' | state-refreshing | refreshing status fetch data | | refreshed | 'refreshed' | state-refreshed | refreshed | | reset | 'reset' | state-reset | reset status | | loading | 'loading' | state-loading | fetching data | init/reset -> pulling -> enough -> refreshing -> refreshed -> reset init/reset -> pulling -> reset init/reset -> loading -> reset # Custom UI components Please refer to the default HeadNode and FooterNode components [ReactPullLoad HeadNode](./src/HeadNode.jsx) [ReactPullLoad FooterNode](./src/FooterNode.jsx) Or refer to demo3, show different dom style through compare props loaderState width STATS. [demo3](https://react-ld.github.io/react-pullLoad/index3.html) # License MIT ================================================ FILE: example/App.css ================================================ html,body{margin: 0; padding: 0;} li{font-size: 20px; width: 100%;list-style: none;} img{width: 100%;} div, .test-ul, p{margin: 0; padding: 0;} .block{position: absolute; top:0; left:0; box-sizing: border-box; height: 100%;box-sizing: border-box;} button{ display: inline-block; font-weight: 500; text-align: center; -ms-touch-action: manipulation; touch-action: manipulation; cursor: pointer; background-image: none; border: 1px solid transparent; white-space: nowrap; line-height: 1.5; padding: 4px 15px; font-size: 12px; border-radius: 4px; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; -webkit-transition: all .3s cubic-bezier(.645,.045,.355,1); transition: all .3s cubic-bezier(.645,.045,.355,1); position: relative; color: rgba(0,0,0,.65); background-color: #fff; border-color: #d9d9d9; outline: 0; margin-right: 8px; margin-bottom: 12px; margin-top: 12px; -webkit-appearance: button; box-sizing: border-box; } ================================================ FILE: example/App1.jsx ================================================ import React, { Component, PureComponent } from 'react' import PropTypes from 'prop-types'; import { render } from 'react-dom' import ReactPullLoad,{STATS} from 'index.js' import '../src/ReactPullLoad.less' import './App.css' const defaultStyle ={ width: "100%", textAlign: "center", fontSize: "20px", lineHeight: "1.5" } const loadMoreLimitNum = 2; const cData = [ "http://img1.gtimg.com/15/1580/158031/15803178_1200x1000_0.jpg", "http://img1.gtimg.com/15/1580/158031/15803179_1200x1000_0.jpg", "http://img1.gtimg.com/15/1580/158031/15803181_1200x1000_0.jpg", "http://img1.gtimg.com/15/1580/158031/15803182_1200x1000_0.jpg", "http://img1.gtimg.com/15/1580/158031/15803183_1200x1000_0.jpg", // "http://img1.gtimg.com/15/1580/158031/15803184_1200x1000_0.jpg", // "http://img1.gtimg.com/15/1580/158031/15803186_1200x1000_0.jpg" ] export class App extends Component{ constructor(){ super(); this.state ={ hasMore: true, data: cData, action: STATS.init, index: loadMoreLimitNum //loading more test time limit } } handleAction = (action) => { console.info(action, this.state.action,action === this.state.action); //new action must do not equel to old action if(action === this.state.action || action === STATS.refreshing && this.state.action === STATS.loading || action === STATS.loading && this.state.action === STATS.refreshing){ // console.info("It's same action or on loading or on refreshing ",action, this.state.action,action === this.state.action); return false } if(action === STATS.refreshing){//刷新 setTimeout(()=>{ //refreshing complete this.setState({ data: cData, hasMore: true, action: STATS.refreshed, index: loadMoreLimitNum }); }, 3000) } else if(action === STATS.loading){//加载更多 this.setState({ hasMore: true }); setTimeout(()=>{ if(this.state.index === 0){ this.setState({ action: STATS.reset, hasMore: false }); } else{ this.setState({ data: [...this.state.data, cData[0], cData[0]], action: STATS.reset, index: this.state.index - 1 }); } }, 3000) } //DO NOT modify below code this.setState({ action: action }) } getScrollTop = ()=>{ if(this.refs.reactpullload){ console.info(this.refs.reactpullload.getScrollTop()); } } setScrollTop = ()=>{ if(this.refs.reactpullload){ console.info(this.refs.reactpullload.setScrollTop(100)); } } render(){ const { data, hasMore } = this.state const fixHeaderStyle = { position: "fixed", width: "100%", height: "50px", color: "#fff", lineHeight: "50px", backgroundColor: "#e24f37", left: 0, top: 0, textAlign: "center", zIndex: 1 } const fixButtonStyle = { position: "fixed", top: 200, width: "100%", } return (
fixed header
) } } render( , document.getElementById('root') ) ================================================ FILE: example/App2.jsx ================================================ import React, { Component, PureComponent } from 'react' import PropTypes from 'prop-types'; import { render } from 'react-dom' import ReactPullLoad,{STATS} from 'index.js' import '../src/ReactPullLoad.less' import './App.css' const defaultStyle ={ width: "100%", textAlign: "center", fontSize: "20px", lineHeight: "1.5" } const loadMoreLimitNum = 2; const cData = [ "http://img1.gtimg.com/15/1580/158031/15803178_1200x1000_0.jpg", "http://img1.gtimg.com/15/1580/158031/15803179_1200x1000_0.jpg", "http://img1.gtimg.com/15/1580/158031/15803181_1200x1000_0.jpg", "http://img1.gtimg.com/15/1580/158031/15803182_1200x1000_0.jpg", "http://img1.gtimg.com/15/1580/158031/15803183_1200x1000_0.jpg", // "http://img1.gtimg.com/15/1580/158031/15803184_1200x1000_0.jpg", // "http://img1.gtimg.com/15/1580/158031/15803186_1200x1000_0.jpg" ] export class App extends Component{ constructor(){ super(); this.state ={ hasMore: true, data: cData, action: STATS.init, index: loadMoreLimitNum //loading more test time limit } } handleAction = (action) => { console.info(action, this.state.action,action === this.state.action); //new action must do not equel to old action if(action === this.state.action || action === STATS.refreshing && this.state.action === STATS.loading || action === STATS.loading && this.state.action === STATS.refreshing){ console.info("It's same action or on loading or on refreshing ",action, this.state.action,action === this.state.action); return false } if(action === STATS.refreshing){//刷新 setTimeout(()=>{ //refreshing complete this.setState({ data: cData, hasMore: true, action: STATS.refreshed, index: loadMoreLimitNum }); }, 3000) } else if(action === STATS.loading){//加载更多 this.setState({ hasMore: true }); setTimeout(()=>{ if(this.state.index === 0){ this.setState({ action: STATS.reset, hasMore: false }); } else{ this.setState({ data: [...this.state.data, cData[0], cData[0]], action: STATS.reset, index: this.state.index - 1 }); } }, 3000) } //DO NOT modify below code this.setState({ action: action }) } render(){ const { data, hasMore } = this.state return (
    { data.map( (str, index )=>{ return
  • }) }
) } } render( , document.getElementById('root') ) ================================================ FILE: example/App3.jsx ================================================ import React, { Component, PureComponent } from 'react' import PropTypes from 'prop-types'; import { render } from 'react-dom' import ReactPullLoad,{STATS} from 'index.js' import '../src/ReactPullLoad.less' import './App.css' const defaultStyle ={ width: "100%", textAlign: "center", fontSize: "20px", lineHeight: "1.5" } class HeadNode extends PureComponent{ static propTypes = { loaderState: PropTypes.string.isRequired, }; static defaultProps = { loaderState: STATS.init, }; render(){ const { loaderState } = this.props let content = "" if(loaderState == STATS.pulling){ content = "下拉刷新" } else if(loaderState == STATS.enough){ content = "松开刷新" } else if(loaderState == STATS.refreshing){ content = "正在刷新..." } else if(loaderState == STATS.refreshed){ content = "刷新成功" } return(
{content}
) } } class FooterNode extends PureComponent{ static propTypes = { loaderState: PropTypes.string.isRequired, hasMore: PropTypes.bool.isRequired }; static defaultProps = { loaderState: STATS.init, hasMore: true }; render(){ const { loaderState, hasMore } = this.props let content = "" // if(hasMore === false){ // content = "没有更多" // } else if(loaderState == STATS.loading && hasMore === true){ // content = "加载中" // } if(loaderState == STATS.loading){ content = "加载中" } else if(hasMore === false){ content = "没有更多" } return(
{content}
) } } const loadMoreLimitNum = 2; const cData = [ "http://img1.gtimg.com/15/1580/158031/15803178_1200x1000_0.jpg", "http://img1.gtimg.com/15/1580/158031/15803179_1200x1000_0.jpg", "http://img1.gtimg.com/15/1580/158031/15803181_1200x1000_0.jpg", "http://img1.gtimg.com/15/1580/158031/15803182_1200x1000_0.jpg", "http://img1.gtimg.com/15/1580/158031/15803183_1200x1000_0.jpg", // "http://img1.gtimg.com/15/1580/158031/15803184_1200x1000_0.jpg", // "http://img1.gtimg.com/15/1580/158031/15803186_1200x1000_0.jpg" ] export class App extends Component{ constructor(){ super(); this.state ={ hasMore: true, data: cData, action: STATS.init, index: loadMoreLimitNum //loading more test time limit } } handleAction = (action) => { //new action must do not equel to old action if(action === this.state.action || action === STATS.refreshing && this.state.action === STATS.loading || action === STATS.loading && this.state.action === STATS.refreshing){ console.info("It's same action or on loading or on refreshing ",action, this.state.action,action === this.state.action); return false } if(action === STATS.refreshing){//刷新 setTimeout(()=>{ //refreshing complete this.setState({ data: cData, hasMore: true, action: STATS.refreshed, index: loadMoreLimitNum }); }, 3000) } else if(action === STATS.loading && this.state.hasMore){//加载更多 setTimeout(()=>{ if(this.state.index === 0){ this.setState({ action: STATS.reset, hasMore: false }); } else{ this.setState({ data: [...this.state.data, cData[0], cData[0]], action: STATS.reset, index: this.state.index - 1 }); } }, 3000) } //无更多内容,不再加载数据 if(action === STATS.loading && !this.state.hasMore){ return; } //DO NOT modify below code this.setState({ action: action }) } render(){ const { data, hasMore } = this.state const fixHeaderStyle = { position: "fixed", width: "100%", height: "50px", color: "#fff", lineHeight: "50px", backgroundColor: "#e24f37", left: 0, top: 0, textAlign: "center", zIndex: 1 } return (
fixed header
    { data.map( (str, index )=>{ return
  • }) }
) } } render( , document.getElementById('root') ) ================================================ FILE: example/App4.jsx ================================================ import React, { Component, PureComponent } from 'react' import PropTypes from 'prop-types'; import { render } from 'react-dom' import ReactPullLoad,{STATS} from 'index.js' import '../src/ReactPullLoad.less' import './App.css' const defaultStyle ={ width: "100%", textAlign: "center", fontSize: "20px", lineHeight: "1.5" } const loadMoreLimitNum = 2; const cData = [ "http://img1.gtimg.com/15/1580/158031/15803178_1200x1000_0.jpg", "http://img1.gtimg.com/15/1580/158031/15803179_1200x1000_0.jpg", "http://img1.gtimg.com/15/1580/158031/15803181_1200x1000_0.jpg", "http://img1.gtimg.com/15/1580/158031/15803182_1200x1000_0.jpg", "http://img1.gtimg.com/15/1580/158031/15803183_1200x1000_0.jpg", // "http://img1.gtimg.com/15/1580/158031/15803184_1200x1000_0.jpg", // "http://img1.gtimg.com/15/1580/158031/15803186_1200x1000_0.jpg" ] export class App extends Component{ constructor(){ super(); this.state ={ hasMore: true, data: cData, action: STATS.init, index: loadMoreLimitNum //loading more test time limit } } handleAction = (action) => { console.info(action, this.state.action,action === this.state.action); if(action !== STATS.loading){ return false; } this.setState({ hasMore: true }); setTimeout(()=>{ if(this.state.index === 0){ this.setState({ action: STATS.reset, hasMore: false }); } else{ this.setState({ data: [...this.state.data, cData[0], cData[0]], action: STATS.reset, index: this.state.index - 1 }); } }, 3000) //DO NOT modify below code this.setState({ action: action }) } getScrollTop = ()=>{ if(this.refs.reactpullload){ console.info(this.refs.reactpullload.getScrollTop()); } } setScrollTop = ()=>{ if(this.refs.reactpullload){ console.info(this.refs.reactpullload.setScrollTop(100)); } } render(){ const { data, hasMore } = this.state const fixHeaderStyle = { position: "fixed", width: "100%", height: "50px", color: "#fff", lineHeight: "50px", backgroundColor: "#e24f37", left: 0, top: 0, textAlign: "center", zIndex: 1 } const fixButtonStyle = { position: "fixed", top: 200, width: "100%", } return (
fixed header
    { data.map( (str, index )=>{ return
  • }) }
) } } render( , document.getElementById('root') ) ================================================ FILE: example/README.md ================================================ # 示例 [demo1](https://react-ld.github.io/react-pullLoad/index1.html) ReactPullLoad 根节点 DOM 作为容器 [demo2](https://react-ld.github.io/react-pullLoad/index2.html) ReactPullLoad 根节点 DOM 作为容器 [demo3](https://react-ld.github.io/react-pullLoad/index3.html) document.body 作为容器 且自定义刷新和加载更多 UI 组件 [demo4](https://react-ld.github.io/react-pullLoad/index4.html) 禁用下拉刷新功能 # 文档 [react-pullLoad](https://github.com/react-ld/react-pullLoad) ================================================ FILE: example/index1.html ================================================ ReactPullLoad demo1
================================================ FILE: example/index2.html ================================================ ReactPullLoad demo2
================================================ FILE: example/index3.html ================================================ ReactPullLoad demo3
================================================ FILE: example/index4.html ================================================ ReactPullLoad demo4
================================================ FILE: gulpfile.js ================================================ var gulp = require('gulp'); var webpack = require('webpack'); var clean = require('gulp-clean'); var gutil = require('gulp-util'); // var ftp = require( 'vinyl-ftp' ); var ghPages = require('gulp-gh-pages'); // var deploy = require('./deploy.config.json'); var deploy_remote_path = "/public/17zt/viewer" var webpack_config_demo = require('./webpack.config.example.js'); var babel = require('gulp-babel'); var less = require('gulp-less'); var path = require('path'); gulp.task('demo:clean', function(){ return gulp.src('./demo', {read: false}) .pipe(clean()); }) gulp.task('demo:file', ['demo:clean'], function(){ return gulp.src(['example/**/*.html','example/README.md']) .pipe(gulp.dest('demo/')) }) //编译示例 gulp.task('demo:webpack', ['demo:clean'], function(callback) { webpack(webpack_config_demo, function (error,status) { //gulp 异步任务必须明确执行 callback() 否则 gulp 将一直卡住 callback() }); }); gulp.task('demo:build', ['demo:file', 'demo:webpack']); //部署示例到自己的测试服务器 // gulp.task('deploy:demo', ['build:demo'], function () { // deploy.log = gutil.log; // var conn = ftp.create(deploy); // return gulp.src('demo/**') // .pipe(conn.dest(deploy_remote_path)) // }) //部署示例到 gh-pages gulp.task('deploy:gh-pages', ['demo:build'], function() { return gulp.src('./demo/**') .pipe(ghPages()); }); gulp.task("publish:clean", function(){ return gulp.src('./dist', {read: false}) .pipe(clean()); }) gulp.task("publish:ts", ["publish:clean"], function(){ return gulp.src('src/**/*.ts') .pipe(gulp.dest('dist')); }) //编译 js 文件 gulp.task('publish:js', ["publish:clean"], function(){ return gulp.src('src/**/*.{js,jsx}') .pipe(babel({ presets: ["es2015", "stage-1", "react"] })) .pipe(gulp.dest('dist')); }) //编译 less 文件 gulp.task('publish:less', ["publish:clean"], function () { return gulp.src('src/**/*.less') .pipe(less({ paths: [ path.join(__dirname, 'less', 'includes') ] })) .pipe(gulp.dest('dist')); }); //发布 css 文件 gulp.task('publish:css', ["publish:clean"], function(){ return gulp.src('src/**/*.css') .pipe(gulp.dest('dist')) }) //打包发布 npm gulp.task('publish', ["publish:clean", 'publish:ts', 'publish:js', 'publish:less']); gulp.task('demo', ['deploy:demo']); gulp.task('gh-pages', ['deploy:gh-pages']); ================================================ FILE: lib/FooterNode.js ================================================ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); 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; }; }(); var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _constants = require('./constants'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 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; } 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; } var FooterNode = function (_PureComponent) { _inherits(FooterNode, _PureComponent); function FooterNode() { _classCallCheck(this, FooterNode); return _possibleConstructorReturn(this, (FooterNode.__proto__ || Object.getPrototypeOf(FooterNode)).apply(this, arguments)); } _createClass(FooterNode, [{ key: 'render', value: function render() { var _props = this.props; var loaderState = _props.loaderState; var hasMore = _props.hasMore; var className = 'pull-load-footer-default ' + (hasMore ? "" : "nomore"); return _react2.default.createElement( 'div', { className: className }, loaderState === _constants.STATS.loading ? _react2.default.createElement('i', null) : "" ); } }]); return FooterNode; }(_react.PureComponent); FooterNode.propTypes = { loaderState: _react.PropTypes.string.isRequired, hasMore: _react.PropTypes.bool.isRequired }; FooterNode.defaultProps = { loaderState: _constants.STATS.init, hasMore: true }; exports.default = FooterNode; ================================================ FILE: lib/HeadNode.js ================================================ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); 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; }; }(); var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _constants = require('./constants'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 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; } 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; } var HeadNode = function (_PureComponent) { _inherits(HeadNode, _PureComponent); function HeadNode() { _classCallCheck(this, HeadNode); return _possibleConstructorReturn(this, (HeadNode.__proto__ || Object.getPrototypeOf(HeadNode)).apply(this, arguments)); } _createClass(HeadNode, [{ key: 'render', value: function render() { var loaderState = this.props.loaderState; return _react2.default.createElement( 'div', { className: 'pull-load-head-default' }, _react2.default.createElement('i', null) ); } }]); return HeadNode; }(_react.PureComponent); HeadNode.propTypes = { loaderState: _react.PropTypes.string.isRequired }; HeadNode.defaultProps = { loaderState: _constants.STATS.init }; exports.default = HeadNode; ================================================ FILE: lib/ReactPullLoad.js ================================================ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); 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; }; 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; }; }(); var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _reactDom = require('react-dom'); var _constants = require('./constants'); var _HeadNode = require('./HeadNode'); var _HeadNode2 = _interopRequireDefault(_HeadNode); var _FooterNode = require('./FooterNode'); var _FooterNode2 = _interopRequireDefault(_FooterNode); require('./ReactPullLoad.less'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 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; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 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; } 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; } function addEvent(obj, type, fn) { if (obj.attachEvent) { obj['e' + type + fn] = fn; obj[type + fn] = function () { obj['e' + type + fn](window.event); }; obj.attachEvent('on' + type, obj[type + fn]); } else obj.addEventListener(type, fn, false); } function removeEvent(obj, type, fn) { if (obj.detachEvent) { obj.detachEvent('on' + type, obj[type + fn]); obj[type + fn] = null; } else obj.removeEventListener(type, fn, false); } var ReactPullLoad = function (_Component) { _inherits(ReactPullLoad, _Component); function ReactPullLoad() { var _ref; var _temp, _this, _ret; _classCallCheck(this, ReactPullLoad); for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = ReactPullLoad.__proto__ || Object.getPrototypeOf(ReactPullLoad)).call.apply(_ref, [this].concat(args))), _this), _this.state = { pullHeight: 0 }, _this.getScrollTop = function () { if (_this.defaultConfig.container) { return _this.defaultConfig.container.scrollTop; } else { return 0; } }, _this.setScrollTop = function (value) { if (_this.defaultConfig.container) { var scrollH = _this.defaultConfig.container.scrollHeight; if (value < 0) { value = 0; } if (value > scrollH) { value = scrollH; } return _this.defaultConfig.container.scrollTop = value; } else { return 0; } }, _this.easing = function (distance) { // t: current time, b: begInnIng value, c: change In value, d: duration var t = distance; var b = 0; var d = screen.availHeight; // 允许拖拽的最大距离 var c = d / 2.5; // 提示标签最大有效拖拽距离 return c * Math.sin(t / d * (Math.PI / 2)) + b; }, _this.canRefresh = function () { return [_constants.STATS.refreshing, _constants.STATS.loading].indexOf(_this.props.action) < 0; }, _this.onPullDownMove = function (data) { if (!_this.canRefresh()) return false; var loaderState = void 0, diff = data[0].touchMoveY - data[0].touchStartY; if (diff < 0) { diff = 0; } diff = _this.easing(diff); if (diff > _this.defaultConfig.downEnough) { loaderState = _constants.STATS.enough; } else { loaderState = _constants.STATS.pulling; } _this.setState({ pullHeight: diff }); _this.props.handleAction(loaderState); }, _this.onPullDownRefresh = function () { if (!_this.canRefresh()) return false; if (_this.props.action === _constants.STATS.pulling) { _this.setState({ pullHeight: 0 }); _this.props.handleAction(_constants.STATS.reset); } else { _this.setState({ pullHeight: 0 }); _this.props.handleAction(_constants.STATS.refreshing); } }, _this.onPullUpMove = function (data) { if (!_this.canRefresh()) return false; // const { hasMore, onLoadMore} = this.props // if (this.props.hasMore) { _this.setState({ pullHeight: 0 }); _this.props.handleAction(_constants.STATS.loading); // } }, _this.onTouchStart = function (event) { var targetEvent = event.changedTouches[0]; _this.startX = targetEvent.clientX; _this.startY = targetEvent.clientY; }, _this.onTouchMove = function (event) { var scrollTop = _this.defaultConfig.container.scrollTop, scrollH = _this.defaultConfig.container.scrollHeight, conH = _this.defaultConfig.container === document.body ? document.documentElement.clientHeight : _this.defaultConfig.container.offsetHeight, targetEvent = event.changedTouches[0], curX = targetEvent.clientX, curY = targetEvent.clientY, diffX = curX - _this.startX, diffY = curY - _this.startY; //判断垂直移动距离是否大于5 && 横向移动距离小于纵向移动距离 if (Math.abs(diffY) > 5 && Math.abs(diffY) > Math.abs(diffX)) { //滚动距离小于设定值 &&回调onPullDownMove 函数,并且回传位置值 if (diffY > 5 && scrollTop < _this.defaultConfig.offsetScrollTop) { //阻止执行浏览器默认动作 event.preventDefault(); _this.onPullDownMove([{ touchStartY: _this.startY, touchMoveY: curY }]); } //滚动距离距离底部小于设定值 else if (diffY < 0 && scrollH - scrollTop - conH < _this.defaultConfig.distanceBottom) { //阻止执行浏览器默认动作 // event.preventDefault(); _this.onPullUpMove([{ touchStartY: _this.startY, touchMoveY: curY }]); } } }, _this.onTouchEnd = function (event) { var scrollTop = _this.defaultConfig.container.scrollTop, targetEvent = event.changedTouches[0], curX = targetEvent.clientX, curY = targetEvent.clientY, diffX = curX - _this.startX, diffY = curY - _this.startY; //判断垂直移动距离是否大于5 && 横向移动距离小于纵向移动距离 if (Math.abs(diffY) > 5 && Math.abs(diffY) > Math.abs(diffX)) { if (diffY > 5 && scrollTop < _this.defaultConfig.offsetScrollTop) { //回调onPullDownRefresh 函数,即满足刷新条件 _this.onPullDownRefresh(); } } }, _temp), _possibleConstructorReturn(_this, _ret); } //set props default values _createClass(ReactPullLoad, [{ key: 'componentDidMount', // container = null; value: function componentDidMount() { var _props = this.props; var isBlockContainer = _props.isBlockContainer; var offsetScrollTop = _props.offsetScrollTop; var downEnough = _props.downEnough; var distanceBottom = _props.distanceBottom; this.defaultConfig = { container: isBlockContainer ? (0, _reactDom.findDOMNode)(this) : document.body, offsetScrollTop: offsetScrollTop, downEnough: downEnough, distanceBottom: distanceBottom }; // console.info("downEnough = ", downEnough, this.defaultConfig.downEnough) /* As below reason handle touch event self ( widthout react defualt touch) Unable to preventDefault inside passive event listener due to target being treated as passive. See https://www.chromestatus.com/features/5093566007214080 */ addEvent(this.refs.container, "touchstart", this.onTouchStart); addEvent(this.refs.container, "touchmove", this.onTouchMove); addEvent(this.refs.container, "touchend", this.onTouchEnd); } // 未考虑到 children 及其他 props 改变的情况 // shouldComponentUpdate(nextProps, nextState) { // if(this.props.action === nextProps.action && this.state.pullHeight === nextState.pullHeight){ // //console.info("[ReactPullLoad] info new action is equal to old action",this.state.pullHeight,nextState.pullHeight); // return false // } else{ // return true // } // } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { removeEvent(this.refs.container, "touchstart", this.onTouchStart); removeEvent(this.refs.container, "touchmove", this.onTouchMove); removeEvent(this.refs.container, "touchend", this.onTouchEnd); } }, { key: 'componentWillReceiveProps', value: function componentWillReceiveProps(nextProps) { var _this2 = this; if (nextProps.action === _constants.STATS.refreshed) { setTimeout(function () { _this2.props.handleAction(_constants.STATS.reset); }, 1000); } } // 拖拽的缓动公式 - easeOutSine }, { key: 'render', value: function render() { var _props2 = this.props; var children = _props2.children; var action = _props2.action; var handleAction = _props2.handleAction; var hasMore = _props2.hasMore; var className = _props2.className; var offsetScrollTop = _props2.offsetScrollTop; var downEnough = _props2.downEnough; var distanceBottom = _props2.distanceBottom; var isBlockContainer = _props2.isBlockContainer; var HeadNode = _props2.HeadNode; var FooterNode = _props2.FooterNode; var other = _objectWithoutProperties(_props2, ['children', 'action', 'handleAction', 'hasMore', 'className', 'offsetScrollTop', 'downEnough', 'distanceBottom', 'isBlockContainer', 'HeadNode', 'FooterNode']); var pullHeight = this.state.pullHeight; var msgStyle = pullHeight ? { WebkitTransform: 'translate3d(0, ' + pullHeight + 'px, 0)', transform: 'translate3d(0, ' + pullHeight + 'px, 0)' } : null; var boxClassName = className + ' pull-load state-' + action; return _react2.default.createElement( 'div', _extends({}, other, { className: boxClassName, ref: 'container' }), _react2.default.createElement( 'div', { className: 'pull-load-body', style: msgStyle }, _react2.default.createElement( 'div', { className: 'pull-load-head' }, _react2.default.createElement(HeadNode, { loaderState: action }) ), children, _react2.default.createElement( 'div', { className: 'pull-load-footer' }, _react2.default.createElement(FooterNode, { loaderState: action, hasMore: hasMore }) ) ) ); } }]); return ReactPullLoad; }(_react.Component); ReactPullLoad.propTypes = { action: _react.PropTypes.string.isRequired, //用于同步状态 handleAction: _react.PropTypes.func.isRequired, //用于处理状态 hasMore: _react.PropTypes.bool, //是否还有更多内容可加载 offsetScrollTop: _react.PropTypes.number, //必须大于零,使触发刷新往下偏移,隐藏部分顶部内容 downEnough: _react.PropTypes.number, //下拉满足刷新的距离 distanceBottom: _react.PropTypes.number, //距离底部距离触发加载更多 isBlockContainer: _react.PropTypes.bool, HeadNode: _react.PropTypes.any, //refresh message react dom FooterNode: _react.PropTypes.any }; ReactPullLoad.defaultProps = { hasMore: true, offsetScrollTop: 1, downEnough: 100, distanceBottom: 100, isBlockContainer: false, className: "", HeadNode: _HeadNode2.default, //refresh message react dom FooterNode: _FooterNode2.default }; exports.default = ReactPullLoad; ================================================ FILE: lib/ReactPullLoad.less ================================================ @transition-duration: .2s; //pull-load container .pull-load{ position: relative; overflow-y: scroll; -webkit-overflow-scrolling: touch; } //head load more msg and refreshing UI .pull-load-head{ position: absolute; transform: translate3d(0px, -100%, 0px); width: 100%; .state-refreshing &, .state-refreshed &{ position: relative; transform: none; } } //body container content .pull-load-body{ // transform: translate3d(0,0,0);// make over the msg-refreshed position: relative; .state-refreshing &{ // transform: translate3d(0,@height,0); transition: transform @transition-duration; } .state-refreshed &{ // handle resolve within 1s // animation: refreshed @transition-duration*5; } .state-reset &{ transition: transform @transition-duration; } } /* * HeadNode default UI */ @bg-dark: #EFEFF4; @height: 3rem; @fontSize: 12px; @fontColor: darken(@bg-dark, 40%);// state hint @btnColor: darken(@bg-dark, 60%);// load more @pullingMsg: '下拉刷新'; @pullingEnoughMsg: '松开刷新'; @refreshingMsg: '正在刷新...'; @refreshedMsg: '刷新成功'; @loadingMsg: '正在加载...'; @btnLoadMore: '加载更多'; @btnLoadNoMore: '没有更多'; .ui-loading(){ display: inline-block; vertical-align: middle; font-size: 1.5rem; width: 1em; height: 1em; border: 2px solid darken(@bg-dark, 30%); border-top-color: #fff; border-radius: 100%; animation: circle .8s infinite linear; } .pull-load-head-default{ text-align: center; font-size: @fontSize; line-height: @height; color: @fontColor; &:after{ .state-pulling &{ content: @pullingMsg } .state-pulling.enough &{ content: @pullingEnoughMsg; } .state-refreshing &{ content: @refreshingMsg; } .state-refreshed &{ content: @refreshedMsg; } } .state-pulling &{ opacity: 1; // arrow down icon i{ display: inline-block; font-size: 2em; margin-right: .6em; vertical-align: middle; height: 1em; border-left: 1px solid; position: relative; transition: transform .3s ease; &:before,&:after{ content: ''; position: absolute; font-size: .5em; width: 1em; bottom: 0px; border-top: 1px solid; } &:before{ right: 1px; transform: rotate(50deg); transform-origin: right; } &:after{ left: 0px; transform: rotate(-50deg); transform-origin: left; } } } .state-pulling.enough &{ // arrow up i{ transform: rotate(180deg); } } .state-refreshing &{ i{ margin-right: 10px; .ui-loading(); } } // 刷新成功提示消息 .state-refreshed &{ opacity: 1; transition: opacity 1s; // √ icon i{ display: inline-block; box-sizing: content-box; vertical-align: middle; margin-right: 10px; font-size: 20px; height: 1em; width: 1em; border: 1px solid; border-radius: 100%; position: relative; &:before{ content: ''; position: absolute; top: 3px; left: 7px; height: 11px; width: 5px; border: solid; border-width: 0 1px 1px 0; transform: rotate(40deg); } } } } .pull-load-footer-default{ text-align: center; font-size: @fontSize; line-height: @height; color: @fontColor; &:after{ .state-loading &{ content: @btnLoadMore; } } &.nomore:after{ content: @btnLoadNoMore; } .state-loading &{ i{ margin-right: 10px; .ui-loading(); } } } // loading效果 @keyframes circle { 100% { transform: rotate(360deg); } } ================================================ FILE: lib/constants.js ================================================ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var STATS = exports.STATS = { init: '', pulling: 'pulling', enough: 'pulling enough', refreshing: 'refreshing', refreshed: 'refreshed', reset: 'reset', loading: 'loading' // loading more }; ================================================ FILE: lib/index.js ================================================ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.STATS = undefined; var _constants = require('./constants'); Object.defineProperty(exports, 'STATS', { enumerable: true, get: function get() { return _constants.STATS; } }); var _ReactPullLoad = require('./ReactPullLoad'); var _ReactPullLoad2 = _interopRequireDefault(_ReactPullLoad); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } exports.default = _ReactPullLoad2.default; ================================================ FILE: package.json ================================================ { "name": "react-pullload", "version": "1.2.0", "description": "React compopnent pull down refresh and pull up load more", "main": "./dist/index.js", "scripts": { "start": "webpack-dev-server --config webpack.config.js", "example": "rm -rf ./demo/* & NODE_ENV=development webpack --config webpack.config.example.js", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/react-ld/react-pullLoad.git" }, "keywords": [ "react", "refresh", "component", "loadmore" ], "author": "lidianhao123", "license": "MIT", "bugs": { "url": "https://github.com/react-ld/react-pullLoad/issues" }, "files": [ "dist", "example", "src" ], "homepage": "https://github.com/react-ld/react-pullLoad#readme", "devDependencies": { "autoprefixer": "^6.5.1", "babel-cli": "^6.16.0", "babel-core": "^6.17.0", "babel-loader": "^6.2.5", "babel-polyfill": "^6.16.0", "babel-preset-es2015": "^6.16.0", "babel-preset-react": "^6.16.0", "babel-preset-stage-1": "^6.16.0", "css-loader": "^0.25.0", "gulp": "^3.9.1", "gulp-babel": "^7.0.0", "gulp-clean": "^0.3.2", "gulp-gh-pages": "git@github.com:tekd/gulp-gh-pages.git#update-dependency", "gulp-less": "^3.3.2", "gulp-util": "^3.0.8", "html-webpack-plugin": "^2.22.0", "less": "^2.7.1", "less-loader": "^2.2.3", "postcss": "^5.2.4", "postcss-loader": "^0.13.0", "react": "^16.0.0", "react-dom": "^16.0.0", "react-hot-loader": "^3.0.0-beta.6", "style-loader": "^0.13.1", "webpack": "^2.6.1", "webpack-dev-server": "^2.4.5" }, "dependencies": { "prop-types": "^15.6.0" } } ================================================ FILE: postcss.config.js ================================================ module.exports = ({ file, options, env }) => ({ // parser: file.extname === '.sss' ? 'sugarss' : false, // plugins: { // 'postcss-import': { root: file.dirname }, // 'postcss-cssnext': options.cssnext ? options.cssnext : false, // 'autoprefixer': env == 'production' ? options.autoprefixer : false, // 'cssnano': env === 'production' ? options.cssnano : false // } plugins: [ require('autoprefixer')({ browsers: ["Android >= 4", "iOS >= 7"]}) ] }) ================================================ FILE: src/FooterNode.jsx ================================================ import React, { PureComponent } from 'react' import PropTypes from 'prop-types'; import { STATS } from './constants' export default class FooterNode extends PureComponent{ static propTypes = { loaderState: PropTypes.string.isRequired, hasMore: PropTypes.bool.isRequired }; static defaultProps = { loaderState: STATS.init, hasMore: true }; render(){ const { loaderState, hasMore } = this.props let className = `pull-load-footer-default ${hasMore? "" : "nomore"}` return(
{ loaderState === STATS.loading ? : "" }
) } } ================================================ FILE: src/HeadNode.jsx ================================================ import React, { PureComponent } from 'react' import PropTypes from 'prop-types'; import { STATS } from './constants' export default class HeadNode extends PureComponent{ static propTypes = { loaderState: PropTypes.string.isRequired, }; static defaultProps = { loaderState: STATS.init, }; render(){ const { loaderState } = this.props return(
) } } ================================================ FILE: src/ReactPullLoad.jsx ================================================ import React, { Component } from 'react' import PropTypes from 'prop-types'; import { findDOMNode } from 'react-dom' import { STATS } from './constants' import HeadNode from './HeadNode' import FooterNode from './FooterNode' function addEvent(obj, type, fn) { if (obj.attachEvent) { obj['e' + type + fn] = fn; obj[type + fn] = function () { obj['e' + type + fn](window.event); } obj.attachEvent('on' + type, obj[type + fn]); } else obj.addEventListener(type, fn, false, {passive: false}); } function removeEvent(obj, type, fn) { if (obj.detachEvent) { obj.detachEvent('on' + type, obj[type + fn]); obj[type + fn] = null; } else obj.removeEventListener(type, fn, false); } export default class ReactPullLoad extends Component { static propTypes = { action: PropTypes.string.isRequired, //用于同步状态 handleAction: PropTypes.func.isRequired, //用于处理状态 hasMore: PropTypes.bool, //是否还有更多内容可加载 offsetScrollTop: PropTypes.number,//必须大于零,使触发刷新往下偏移,隐藏部分顶部内容 downEnough: PropTypes.number, //下拉满足刷新的距离 distanceBottom: PropTypes.number, //距离底部距离触发加载更多 isBlockContainer: PropTypes.bool, HeadNode: PropTypes.any, //refresh message react dom FooterNode: PropTypes.any, //refresh loading react dom }; //set props default values static defaultProps = { hasMore: true, offsetScrollTop: 1, downEnough: 100, distanceBottom: 100, isBlockContainer: false, className: "", HeadNode: HeadNode, //refresh message react dom FooterNode: FooterNode, //refresh loading react dom }; state = { pullHeight: 0 }; // container = null; componentDidMount() { const {isBlockContainer, offsetScrollTop, downEnough, distanceBottom} = this.props this.defaultConfig = { container: isBlockContainer ? findDOMNode(this) : document.body, offsetScrollTop: offsetScrollTop, downEnough: downEnough, distanceBottom: distanceBottom }; // console.info("downEnough = ", downEnough, this.defaultConfig.downEnough) /* As below reason handle touch event self ( widthout react defualt touch) Unable to preventDefault inside passive event listener due to target being treated as passive. See https://www.chromestatus.com/features/5093566007214080 */ addEvent(this.refs.container, "touchstart", this.onTouchStart) addEvent(this.refs.container, "touchmove", this.onTouchMove) addEvent(this.refs.container, "touchend", this.onTouchEnd) } // 未考虑到 children 及其他 props 改变的情况 // shouldComponentUpdate(nextProps, nextState) { // if(this.props.action === nextProps.action && this.state.pullHeight === nextState.pullHeight){ // //console.info("[ReactPullLoad] info new action is equal to old action",this.state.pullHeight,nextState.pullHeight); // return false // } else{ // return true // } // } componentWillUnmount() { removeEvent(this.refs.container, "touchstart", this.onTouchStart) removeEvent(this.refs.container, "touchmove", this.onTouchMove) removeEvent(this.refs.container, "touchend", this.onTouchEnd) } componentWillReceiveProps(nextProps) { if(nextProps.action === STATS.refreshed){ setTimeout(()=>{ this.props.handleAction(STATS.reset) },1000) } } getScrollTop = ()=>{ if(this.defaultConfig.container){ if(this.defaultConfig.container === document.body){ return document.documentElement.scrollTop || document.body.scrollTop; } return this.defaultConfig.container.scrollTop; } else{ return 0; } } setScrollTop = (value)=>{ if(this.defaultConfig.container){ let scrollH = this.defaultConfig.container.scrollHeight; if(value < 0){ value = 0} if(value > scrollH){ value = scrollH} return this.defaultConfig.container.scrollTop = value; } else{ return 0; } } // 拖拽的缓动公式 - easeOutSine easing = (distance) => { // t: current time, b: begInnIng value, c: change In value, d: duration var t = distance; var b = 0; var d = screen.availHeight; // 允许拖拽的最大距离 var c = d / 2.5; // 提示标签最大有效拖拽距离 return c * Math.sin(t / d * (Math.PI / 2)) + b; } canRefresh = () => { return [STATS.refreshing, STATS.loading].indexOf(this.props.action) < 0; } onPullDownMove = (data) => { if(!this.canRefresh())return false; let loaderState, diff = data[0].touchMoveY - data[0].touchStartY; if (diff < 0) { diff = 0; } diff = this.easing(diff); if (diff > this.defaultConfig.downEnough) { loaderState = STATS.enough } else { loaderState = STATS.pulling } this.setState({ pullHeight: diff, }) this.props.handleAction(loaderState) } onPullDownRefresh = () => { if(!this.canRefresh())return false; if (this.props.action === STATS.pulling) { this.setState({pullHeight: 0}) this.props.handleAction(STATS.reset) } else { this.setState({ pullHeight: 0, }) this.props.handleAction(STATS.refreshing) } } onPullUpMove = (data) => { if(!this.canRefresh())return false; // const { hasMore, onLoadMore} = this.props // if (this.props.hasMore) { this.setState({ pullHeight: 0, }) this.props.handleAction(STATS.loading) // } } onTouchStart = (event) => { var targetEvent = event.changedTouches[0]; this.startX = targetEvent.clientX; this.startY = targetEvent.clientY; } onTouchMove = (event) => { let scrollTop = this.getScrollTop(), scrollH = this.defaultConfig.container.scrollHeight, conH = this.defaultConfig.container === document.body ? document.documentElement.clientHeight : this.defaultConfig.container.offsetHeight, targetEvent = event.changedTouches[0], curX = targetEvent.clientX, curY = targetEvent.clientY, diffX = curX - this.startX, diffY = curY - this.startY; //判断垂直移动距离是否大于5 && 横向移动距离小于纵向移动距离 if (Math.abs(diffY) > 5 && Math.abs(diffY) > Math.abs(diffX)) { //滚动距离小于设定值 &&回调onPullDownMove 函数,并且回传位置值 if (diffY > 5 && scrollTop < this.defaultConfig.offsetScrollTop) { //阻止执行浏览器默认动作 event.preventDefault(); this.onPullDownMove([{ touchStartY: this.startY, touchMoveY: curY }]); } //滚动距离距离底部小于设定值 else if (diffY < 0 && (scrollH - scrollTop - conH) < this.defaultConfig.distanceBottom) { //阻止执行浏览器默认动作 // event.preventDefault(); this.onPullUpMove([{ touchStartY: this.startY, touchMoveY: curY }]); } } } onTouchEnd = (event) => { let scrollTop = this.getScrollTop(), targetEvent = event.changedTouches[0], curX = targetEvent.clientX, curY = targetEvent.clientY, diffX = curX - this.startX, diffY = curY - this.startY; //判断垂直移动距离是否大于5 && 横向移动距离小于纵向移动距离 if (Math.abs(diffY) > 5 && Math.abs(diffY) > Math.abs(diffX)) { if (diffY > 5 && scrollTop < this.defaultConfig.offsetScrollTop) { //回调onPullDownRefresh 函数,即满足刷新条件 this.onPullDownRefresh(); } } } render() { const { children, action, handleAction, hasMore, className, offsetScrollTop, downEnough, distanceBottom, isBlockContainer, HeadNode, FooterNode, ...other } = this.props const { pullHeight } = this.state const msgStyle = pullHeight ? { WebkitTransform: `translate3d(0, ${pullHeight}px, 0)`, transform: `translate3d(0, ${pullHeight}px, 0)` } : null; const boxClassName = `${className} pull-load state-${action}`; return (
{ children }
) } } ================================================ FILE: src/ReactPullLoad.less ================================================ @transition-duration: .2s; //pull-load container .pull-load{ position: relative; overflow-y: scroll; -webkit-overflow-scrolling: touch; } //head load more msg and refreshing UI .pull-load-head{ position: absolute; transform: translate3d(0px, -100%, 0px); width: 100%; .state-refreshing &, .state-refreshed &{ position: relative; transform: none; } } //body container content .pull-load-body{ // transform: translate3d(0,0,0);// make over the msg-refreshed position: relative; .state-refreshing &{ // transform: translate3d(0,@height,0); transition: transform @transition-duration; } .state-refreshed &{ // handle resolve within 1s // animation: refreshed @transition-duration*5; } .state-reset &{ transition: transform @transition-duration; } } /* * HeadNode default UI */ @bg-dark: #EFEFF4; @height: 3rem; @fontSize: 12px; @fontColor: darken(@bg-dark, 40%);// state hint @btnColor: darken(@bg-dark, 60%);// load more @pullingMsg: '下拉刷新'; @pullingEnoughMsg: '松开刷新'; @refreshingMsg: '正在刷新...'; @refreshedMsg: '刷新成功'; @loadingMsg: '正在加载...'; @btnLoadMore: '加载更多'; @btnLoadNoMore: '没有更多'; .ui-loading(){ display: inline-block; vertical-align: middle; font-size: 1.5rem; width: 1em; height: 1em; border: 2px solid darken(@bg-dark, 30%); border-top-color: #fff; border-radius: 100%; animation: circle .8s infinite linear; } .pull-load-head-default{ text-align: center; font-size: @fontSize; line-height: @height; color: @fontColor; &:after{ .state-pulling &{ content: @pullingMsg } .state-pulling.enough &{ content: @pullingEnoughMsg; } .state-refreshing &{ content: @refreshingMsg; } .state-refreshed &{ content: @refreshedMsg; } } .state-pulling &{ opacity: 1; // arrow down icon i{ display: inline-block; font-size: 2em; margin-right: .6em; vertical-align: middle; height: 1em; border-left: 1px solid; position: relative; transition: transform .3s ease; &:before,&:after{ content: ''; position: absolute; font-size: .5em; width: 1em; bottom: 0px; border-top: 1px solid; } &:before{ right: 1px; transform: rotate(50deg); transform-origin: right; } &:after{ left: 0px; transform: rotate(-50deg); transform-origin: left; } } } .state-pulling.enough &{ // arrow up i{ transform: rotate(180deg); } } .state-refreshing &{ i{ margin-right: 10px; .ui-loading(); } } // 刷新成功提示消息 .state-refreshed &{ opacity: 1; transition: opacity 1s; // √ icon i{ display: inline-block; box-sizing: content-box; vertical-align: middle; margin-right: 10px; font-size: 20px; height: 1em; width: 1em; border: 1px solid; border-radius: 100%; position: relative; &:before{ content: ''; position: absolute; top: 3px; left: 7px; height: 11px; width: 5px; border: solid; border-width: 0 1px 1px 0; transform: rotate(40deg); } } } } .pull-load-footer-default{ text-align: center; font-size: @fontSize; line-height: @height; color: @fontColor; &:after{ .state-loading &{ content: @btnLoadMore; } } &.nomore:after{ content: @btnLoadNoMore; } .state-loading &{ i{ margin-right: 10px; .ui-loading(); } } } // loading效果 @keyframes circle { 100% { transform: rotate(360deg); } } ================================================ FILE: src/constants.js ================================================ export const STATS = { init: '', pulling: 'pulling', enough: 'pulling enough', refreshing: 'refreshing', refreshed: 'refreshed', reset: 'reset', loading: 'loading' // loading more }; ================================================ FILE: src/index.d.ts ================================================ import * as React from "react"; declare enum STATS { init = "", pulling = "pulling", enough = "pulling enough", refreshing = "refreshing", refreshed = "refreshed", reset = "reset", loading = "loading" // loading more } export interface PullLoadProps { action: STATS; //用于同步状态 handleAction: (action: STATS) => void; //用于处理状态 hasMore: boolean; //是否还有更多内容可加载 offsetScrollTop?: number; //必须大于零,使触发刷新往下偏移,隐藏部分顶部内容 downEnough?: number; //下拉满足刷新的距离 distanceBottom?: number; //距离底部距离触发加载更多 isBlockContainer?: boolean; HeadNode?: React.ReactNode | string; //refresh message react dom FooterNode?: React.ReactNode | string; //refresh loading react dom children: React.ReactChild; // 子组件 } export default class ReactPullLoad extends React.Component< PullLoadProps, any > {} ================================================ FILE: src/index.js ================================================ // import { STATS as _STATS } from 'constants' // export const STATS = _STATS // export default ReactPullLoad export { STATS } from './constants' export default from './ReactPullLoad' ================================================ FILE: webpack.config.example.js ================================================ var path = require("path"); var webpack = require("webpack"); var HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { context: path.resolve(__dirname, "example"), // string(绝对路径!) devtool: "eval", cache: true, entry: { bundle1: ["babel-polyfill", "./App1.jsx"], bundle2: ["babel-polyfill", "./App2.jsx"], bundle3: ["babel-polyfill", "./App3.jsx"], bundle4: ["babel-polyfill", "./App4.jsx"] }, output: { path: path.join(__dirname, "demo/"), filename: "[name].js" }, plugins: [ // new webpack.optimize.OccurenceOrderPlugin(), new webpack.DefinePlugin({ "process.env": { NODE_ENV: JSON.stringify("production") } }), new webpack.NamedModulesPlugin(), // 当模块热替换(HMR)时在浏览器控制台输出对用户更友好的模块名字信息 new webpack.optimize.UglifyJsPlugin({ sourceMap: true, compress: { warnings: true } }) // new HtmlWebpackPlugin({ template: 'index.html' }) ], resolve: { extensions: [".js", ".jsx"], modules: ["node_modules", path.resolve(__dirname, "src")] }, module: { rules: [ { test: /\.(js|jsx)$/, loader: "babel-loader", exclude: /node_modules/, include: __dirname, options: { presets: [["es2015", { modules: false }], "stage-1", "react"] } }, { test: /\.css$/, use: [ "style-loader", "css-loader", { loader: "postcss-loader", options: { config: { path: "./postcss.config.js" } } } ] }, { test: /\.less/, use: [ "style-loader", "css-loader", { loader: "postcss-loader", options: { config: { path: "./postcss.config.js" } } }, "less-loader" ] }, { test: /\.(gif|jpg|png|woff|svg|eot|ttf)$/, use: [{ loader: "file-loader" }] } ] } }; ================================================ FILE: webpack.config.js ================================================ var webpack = require("webpack"); var HtmlWebpackPlugin = require("html-webpack-plugin"); var path = require("path"); var port = 3010; var demoNum = 3; module.exports = { context: path.resolve(__dirname, "example"), // string(绝对路径!) devtool: "eval", cache: true, entry: [ "react-hot-loader/patch", // 开启 React 代码的模块热替换(HMR) "webpack-dev-server/client?http://0.0.0.0:" + port, "webpack/hot/only-dev-server", "./App"+demoNum+".jsx" ], plugins: [ new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin(), // 当模块热替换(HMR)时在浏览器控制台输出对用户更友好的模块名字信息 new webpack.optimize.UglifyJsPlugin({ sourceMap: true, compress: { warnings: true } }), new HtmlWebpackPlugin({ title: "Custom template", template: "./index"+demoNum+".html", // Load a custom template (ejs by default see the FAQ for details) hash: true, filename: "./index.html" }) ], resolve: { modules: ["node_modules", path.resolve(__dirname, "src")], extensions: [".js", ".jsx"] }, devServer: { hot: true, // 开启服务器的模块热替换(HMR) host: "0.0.0.0", port: port }, module: { rules: [ { test: /\.(js|jsx)$/, loader: "babel-loader", exclude: /node_modules/, include: __dirname, options: { presets: [["es2015", { modules: false }], "stage-1", "react"], plugins: [ "react-hot-loader/babel" // 开启 React 代码的模块热替换(HMR) ] } }, { test: /\.css$/, use: [ "style-loader", "css-loader", { loader: "postcss-loader", options: { config: { path: "./postcss.config.js" } } } ] }, { test: /\.less/, use: [ "style-loader", "css-loader", { loader: "postcss-loader", options: { config: { path: "./postcss.config.js" } } }, "less-loader" ] }, { test: /\.(gif|jpg|png|woff|svg|eot|ttf)$/, use: [{ loader: "file-loader" }] } ] } };