1. import React from 'react'
  2. import PropTypes from 'prop-types'
  3. import {findDOMNode} from 'react-dom'
  4. import Component from '../utils/Component'
  5. import classnames from 'classnames'
  6. import {setPhPrefix, getClientHeight, getScrollTop, getDocumentHeight, preventDefault} from '../utils/Tool'
  7. import Logger from '../utils/logger'
  8.  
  9. import Button from '../button'
  10. import Icon from '../icon'
  11.  
  12. import '../style'
  13. import 'phoenix-styles/less/modules/pullup.less'
  14.  
  15. /**
  16. * 加载更多组件<br/>
  17. * - 书写时PullUp组件在可加载列表的后面。
  18. * - 通过mode设置加载更多的模式,有点击按钮加载更多,以及滑到最底端自动加载,可选 [auto,button] 2种参数。
  19. * - 通过status设置当前状态,只需要在请求结束返回相应状态,包含请求成功返回2,请求成功并再没有数据返回4,请求失败返回3。
  20. * - 可通过tips设置按钮文字和状态提示语,默认['加载更多','','加载成功','加载失败','没有更多'],分别对应status的状态。
  21. * - 可通过phStyle设置按钮的样式,如果当前mode为auto设置无效。
  22. * - 可通过loadCallback设置点击按钮加载或滑到底部自动加载的回调函数,如果状态为4不执行。
  23. * - 如果当前列表存在自定义的滚动条,需要通过getTarget传递滚动的目标,且滚动元素的子元素必须只有一个。
  24. *
  25. * 主要属性和接口:
  26. * - mode:加载更多的模式,默认auto。
  27. * - status:当前状态:0加载更多, 1加载中, 2数据加载成功, 3数据加载失败, 4没有更多。
  28. * - tips:按钮文字和状态提示语,默认['加载更多','','加载成功','加载失败','没有更多']。
  29. * - phStyle:按钮的样式,默认'primary'。
  30. * - loadCallback:点击按钮加载或滑到底部自动加载的回调函数。
  31. * - getTarget: 如果当前列表存在自定义的滚动条,需要传递滚动的目标。
  32. *
  33. * 示例:
  34. * ```code
  35. * <div style={{height:'300px',overflow:'auto'}} ref={(list)=>this.list=list}> // 用到getTarget需要保证只有一个子元素,包裹住滚动的所有内容
  36. * <div>
  37. * <List>...</List> // 可加载列表的位置
  38. * <PullUp mode='button' status={this.state.status}
  39. * tips={['点击加载更多','加载中...','加载成功!','加载失败!','没有更多']}
  40. * phStyle='primary'
  41. * loadCallback={this.loadCallback.bind(this)}
  42. * getTarget={()=>{return list;}} />
  43. * </div>
  44. * </div>
  45. * ```
  46. *
  47. * @class PullUp
  48. * @module 操作类组件
  49. * @extends Component
  50. * @constructor
  51. * @since 2.0.0
  52. * @demo pullup|pullup.js {展示}
  53. * @show true
  54. * */
  55. const MAX_HEIGHT = 800
  56.  
  57. export default class PullUp extends Component{
  58. static propTypes = {
  59. /**
  60. * 样式前缀
  61. * @property classPrefix
  62. * @type String
  63. * @default 'pullup'
  64. * */
  65. classPrefix: PropTypes.string,
  66. /**
  67. * 加载更多的模式,可选[auto,button], 默认auto
  68. * @property mode
  69. * @type String
  70. * @default 'auto'
  71. **/
  72. mode:PropTypes.string,
  73. /**
  74. * 加载状态:0初始状态, 1加载中, 2数据加载成功, 3数据加载失败, 4没有更多
  75. * @property status
  76. * @type Number
  77. * @default 0
  78. **/
  79. status: PropTypes.number,
  80. /**
  81. * 加载5个状态的文字描述,默认['加载更多','','加载成功','加载失败','没有更多']
  82. * @property tips
  83. * @type Array
  84. * @default ['加载更多','加载中','加载成功','加载失败','没有更多']
  85. **/
  86. tips: PropTypes.array,
  87. /**
  88. * 按钮颜色,默认primary
  89. * @property phStyle
  90. * @type Array
  91. * @default 'primary'
  92. **/
  93. phStyle: PropTypes.string,
  94. /**
  95. * 滑到底部自动加载的回调函数,用户在该函数内自定义请求
  96. * @method loadCallback
  97. * @type Function
  98. * @default null
  99. **/
  100. loadCallback: PropTypes.func,
  101. /**
  102. * 如果当前列表存在自定义的滚动条,需要传递滚动的目标
  103. * @method getTarget
  104. * @type Function
  105. * @default null
  106. * @return {object} 目标元素的ref
  107. **/
  108. getTarget: PropTypes.func
  109. }
  110.  
  111. static defaultProps ={
  112. status: 4, // 0初始状态, 1加载中, 2加载成功, 3加载失败, 4没有更多
  113. mode: 'auto',
  114. phStyle: 'primary',
  115. tips: ['加载更多','','加载成功','加载失败','没有更多'],
  116. classPrefix:'pullup',
  117. hardware: true,
  118. classMapping : {}
  119. }
  120.  
  121. constructor(props,context){ // 记得做数据没有触底的判断
  122. super(props,context)
  123. new Logger('PullUp')
  124.  
  125. this.state = {
  126. status: props.status
  127. }
  128.  
  129. if(props.mode=='button') return
  130.  
  131. this.touchBottom = false
  132. this.distanceY = 0
  133. }
  134.  
  135. scrollHandler(e){
  136. let {status} = this.state,
  137. {getTarget} = this.props,
  138. target = e.target
  139. if(getTarget){
  140. this.scrollTop = target.scrollTop
  141. this.bodyHeight = target.clientHeight
  142. this.documentHeight = target.children[0].offsetHeight
  143. }else{
  144. this.scrollTop = getScrollTop()
  145. this.bodyHeight = getClientHeight()
  146. this.documentHeight = getDocumentHeight()
  147. }
  148. // console.log('this.scrollTop', this.scrollTop)
  149. // console.log('this.bodyHeight', this.bodyHeight)
  150. // console.log('this.documentHeight', this.documentHeight)
  151.  
  152. this.pullTop = this.documentHeight - this.pullUp.offsetHeight
  153. // if(!this.pullHeight) this.pullHeight = this.pullUp.offsetHeight
  154.  
  155. if(this.scrollTop + this.bodyHeight >= this.pullTop){
  156. this.touchBottom = true
  157. if(status==3) return
  158. this.loadCallback()
  159. }else{
  160. this.touchBottom = false
  161. }
  162. }
  163.  
  164. componentWillReceiveProps(nextProps){
  165. if(nextProps.status !== this.state.status){
  166. this.setState({
  167. status: nextProps.status
  168. })
  169. }
  170. }
  171.  
  172. componentDidMount(){
  173. let pullUpElem = findDOMNode(this.pullUp)
  174.  
  175. this.scrollHandle = this.scrollHandler.bind(this)
  176. this.scrollElem = window
  177.  
  178. this.dragElem = pullUpElem.parentNode
  179. this.prevElem = pullUpElem.previousElementSibling
  180. this.addClass(this.dragElem, 'animated')
  181. this.props.hardware && this.addClass(this.prevElem, 'hardware')
  182.  
  183. this.dragEventHandle(this.dragElem)
  184.  
  185. if(this.props.getTarget){
  186. setTimeout(()=>{
  187. this.scrollElem = this.props.getTarget()
  188. this.scrollElem.addEventListener('scroll', this.scrollHandle, false)
  189. },0);
  190. }else{
  191. this.scrollElem.addEventListener('scroll', this.scrollHandle, false)
  192. }
  193. }
  194.  
  195. dragEventHandle(elem){
  196. this.touchStartHandle = this.touchStartHandle.bind(this)
  197. elem.addEventListener('touchstart', this.touchStartHandle, false)
  198.  
  199. this.touchMoveHandle = this.touchMoveHandle.bind(this)
  200. elem.addEventListener('touchmove', this.touchMoveHandle, false)
  201.  
  202. this.touchEndHandle = this.touchEndHandle.bind(this)
  203. elem.addEventListener('touchend', this.touchEndHandle, false)
  204.  
  205. this.touchCancelHandle = this.touchCancelHandle.bind(this)
  206. elem.addEventListener('touchcancel', this.touchCancelHandle, false)
  207. }
  208.  
  209. componentDidUpdate(){
  210. let {status} = this.state
  211.  
  212. // 只有加载成功并传值才重置状态
  213. if(status==2){
  214. this.setState({
  215. status: 0
  216. })
  217. }
  218. }
  219.  
  220. loadCallback(){
  221. this.touchBottom = false
  222.  
  223. let {loadCallback} = this.props,
  224. {status} = this.state
  225.  
  226. // 如果已经没有更多,不再继续请求数据的操作
  227. if(status==4 || status==1) return
  228.  
  229. // 状态置为加载中(状态为0或3时执行)
  230. this.setState({
  231. status: 1
  232. }, ()=>{
  233. if(loadCallback) loadCallback()
  234. })
  235. }
  236.  
  237. touchStartHandle(e){
  238. // preventDefault(e)
  239. if(!this.touchBottom) return
  240. this.distanceY = 0
  241. this.starY = event.touches[0].pageY
  242. }
  243.  
  244. touchMoveHandle(e){
  245. // preventDefault(e)
  246. if(!this.touchBottom) return
  247.  
  248. this.moveY = event.touches[0].pageY
  249. this.distanceY = this.moveY - this.starY
  250.  
  251. if(this.distanceY>=0) return
  252.  
  253. this.distanceY = Math.abs(this.distanceY)
  254.  
  255. this.transform = Math.min(1, MAX_HEIGHT/this.distanceY) * Math.min(MAX_HEIGHT, this.distanceY)
  256. this.dragElem.style.transform = 'translateY('+(-this.transform)+'px)'
  257. }
  258.  
  259. touchEndHandle(e){
  260. // preventDefault(e)
  261. if(!this.touchBottom) return
  262.  
  263. this.starY = this.moveY
  264.  
  265. this.dragElem.style.transform = 'translateY(0)'
  266. if(Math.abs(this.distanceY) <= 80 || this.distanceY>=0) return
  267.  
  268. this.loadCallback()
  269. }
  270.  
  271. touchCancelHandle(){
  272. this.dragElem.style.transform = 'translateY(0)'
  273. }
  274.  
  275. componentWillUnmount(){
  276. if(this.props.mode=='button') return
  277.  
  278. this.scrollElem.removeEventListener('scroll', this.scrollHandle, false)
  279.  
  280. this.dragElem.removeEventListener('touchstart', this.touchStartHandle, false)
  281. this.dragElem.removeEventListener('touchmove', this.touchMoveHandle, false)
  282. this.dragElem.removeEventListener('touchend', this.touchEndHandle, false)
  283. }
  284.  
  285. renderContent(){
  286. let {mode, tips, phStyle} = this.props,
  287. {status} = this.state
  288.  
  289. if(mode=='button'){
  290. return (
  291. <Button phStyle={status==3?'error':phStyle} disabled={status==4||status==1}
  292. onClick={this.loadCallback.bind(this)}>
  293. {this.renderIcon(status)}
  294. {tips[status]}
  295. </Button>
  296. )
  297. }else{
  298. return (
  299. <div className={setPhPrefix('pullup-tip')}>
  300. {this.renderIcon(status)}
  301. {tips[status]}
  302. </div>
  303. )
  304. }
  305. }
  306.  
  307. renderIcon(status){
  308. if(status==1){
  309. return <Icon className='gfs-icon-loading' phIcon='loading-gray' phSize='sm' />;
  310. }
  311. }
  312.  
  313. renderPullUp(){
  314. return (
  315. <div {...this.otherProps} ref={(pullUp)=>{this.pullUp=pullUp}} className={classnames(this.getProperty(true),this.props.className)}>
  316. {this.renderContent()}
  317. </div>
  318. )
  319. }
  320.  
  321. render(){
  322. return this.renderPullUp()
  323. }
  324. }