rmc-pull-to-refresh
Version:
React Mobile Pull To Refresh Component
346 lines (327 loc) • 14.6 kB
JavaScript
import _extends from 'babel-runtime/helpers/extends';
import _classCallCheck from 'babel-runtime/helpers/classCallCheck';
import _createClass from 'babel-runtime/helpers/createClass';
import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn';
import _inherits from 'babel-runtime/helpers/inherits';
var __rest = this && this.__rest || function (s, e) {
var t = {};
for (var p in s) {
if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p];
}if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0) t[p[i]] = s[p[i]];
}return t;
};
import React from 'react';
import classNames from 'classnames';
var StaticRenderer = function (_React$Component) {
_inherits(StaticRenderer, _React$Component);
function StaticRenderer() {
_classCallCheck(this, StaticRenderer);
return _possibleConstructorReturn(this, (StaticRenderer.__proto__ || Object.getPrototypeOf(StaticRenderer)).apply(this, arguments));
}
_createClass(StaticRenderer, [{
key: 'shouldComponentUpdate',
value: function shouldComponentUpdate(nextProps) {
return nextProps.shouldUpdate;
}
}, {
key: 'render',
value: function render() {
return React.createElement(
'div',
null,
this.props.render()
);
}
}]);
return StaticRenderer;
}(React.Component);
function setTransform(nodeStyle, value) {
nodeStyle.transform = value;
nodeStyle.webkitTransform = value;
nodeStyle.MozTransform = value;
}
var isWebView = typeof navigator !== 'undefined' && /(iPhone|iPod|iPad).*AppleWebKit(?!.*Safari)/i.test(navigator.userAgent);
var DOWN = 'down';
var UP = 'up';
var INDICATOR = { activate: 'release', deactivate: 'pull', release: 'loading', finish: 'finish' };
var supportsPassive = false;
try {
var opts = Object.defineProperty({}, 'passive', {
get: function get() {
supportsPassive = true;
}
});
window.addEventListener('test', null, opts);
} catch (e) {
// empty
}
var willPreventDefault = supportsPassive ? { passive: false } : false;
// const willNotPreventDefault = supportsPassive ? { passive: true } : false;
var PullToRefresh = function (_React$Component2) {
_inherits(PullToRefresh, _React$Component2);
function PullToRefresh() {
_classCallCheck(this, PullToRefresh);
// https://github.com/yiminghe/zscroller/blob/2d97973287135745818a0537712235a39a6a62a1/src/Scroller.js#L355
// currSt: `activate` / `deactivate` / `release` / `finish`
var _this2 = _possibleConstructorReturn(this, (PullToRefresh.__proto__ || Object.getPrototypeOf(PullToRefresh)).apply(this, arguments));
_this2.state = {
currSt: 'deactivate',
dragOnEdge: false
};
_this2._isMounted = false;
_this2.shouldUpdateChildren = false;
_this2.triggerPullToRefresh = function () {
// 在初始化时、用代码 自动 触发 pullToRefresh
// 注意:当 direction 为 up 时,当 visible length < content length 时、则看不到效果
// 添加this._isMounted的判断,否则组建一实例化,currSt就会是finish
if (!_this2.state.dragOnEdge && _this2._isMounted) {
if (_this2.props.refreshing) {
if (_this2.props.direction === UP) {
_this2._lastScreenY = -_this2.props.distanceToRefresh - 1;
}
if (_this2.props.direction === DOWN) {
_this2._lastScreenY = _this2.props.distanceToRefresh + 1;
}
// change dom need after setState
_this2.setState({ currSt: 'release' }, function () {
return _this2.setContentStyle(_this2._lastScreenY);
});
} else {
_this2.setState({ currSt: 'finish' }, function () {
return _this2.reset();
});
}
}
};
_this2.init = function (ele) {
if (!ele) {
// like return in destroy fn ???!!
return;
}
_this2._to = {
touchstart: _this2.onTouchStart.bind(_this2, ele),
touchmove: _this2.onTouchMove.bind(_this2, ele),
touchend: _this2.onTouchEnd.bind(_this2, ele),
touchcancel: _this2.onTouchEnd.bind(_this2, ele)
};
Object.keys(_this2._to).forEach(function (key) {
ele.addEventListener(key, _this2._to[key], willPreventDefault);
});
};
_this2.destroy = function (ele) {
if (!_this2._to || !ele) {
// componentWillUnmount fire before componentDidMount, like forceUpdate ???!!
return;
}
Object.keys(_this2._to).forEach(function (key) {
ele.removeEventListener(key, _this2._to[key]);
});
};
_this2.onTouchStart = function (_ele, e) {
_this2._ScreenY = _this2._startScreenY = e.touches[0].screenY;
_this2._startScreenX = e.touches[0].screenX;
// 一开始 refreshing 为 true 时 this._lastScreenY 有值
_this2._lastScreenY = _this2._lastScreenY || 0;
};
_this2.isEdge = function (ele, direction) {
var container = _this2.props.getScrollContainer();
if (container && container === document.body) {
// In chrome61 `document.body.scrollTop` is invalid
var scrollNode = document.scrollingElement ? document.scrollingElement : document.body;
if (direction === UP) {
return scrollNode.scrollHeight - scrollNode.scrollTop <= window.innerHeight;
}
if (direction === DOWN) {
return scrollNode.scrollTop <= 0;
}
}
if (direction === UP) {
return ele.scrollHeight - ele.scrollTop === ele.clientHeight;
}
if (direction === DOWN) {
return ele.scrollTop <= 0;
}
};
_this2.damping = function (dy) {
if (Math.abs(_this2._lastScreenY) > _this2.props.damping) {
return 0;
}
var ratio = Math.abs(_this2._ScreenY - _this2._startScreenY) / window.screen.height;
dy *= (1 - ratio) * _this2.props.scale;
return dy;
};
_this2.onTouchMove = function (ele, e) {
// 使用 pageY 对比有问题
var _screenY = e.touches[0].screenY;
var _screenX = e.touches[0].screenX;
var direction = _this2.props.direction;
// 横向滑动不处理
if (Math.abs(_screenX - _this2._startScreenX) > 20 * window.devicePixelRatio) {
return;
}
// 拖动方向不符合的不处理
if (direction === UP && _this2._startScreenY < _screenY || direction === DOWN && _this2._startScreenY > _screenY) {
return;
}
if (_this2.isEdge(ele, direction)) {
if (!_this2.state.dragOnEdge) {
// 当用户开始往上滑的时候isEdge还是false的话,会导致this._ScreenY不是想要的,只有当isEdge为true时,再上滑,才有意义
// 下面这行代码解决了上面这个问题
_this2._ScreenY = _this2._startScreenY = e.touches[0].screenY;
_this2.setState({ dragOnEdge: true });
}
e.preventDefault();
// add stopPropagation with fastclick will trigger content onClick event. why?
// ref https://github.com/ant-design/ant-design-mobile/issues/2141
// e.stopPropagation();
var _diff = Math.round(_screenY - _this2._ScreenY);
_this2._ScreenY = _screenY;
_this2._lastScreenY += _this2.damping(_diff);
_this2.setContentStyle(_this2._lastScreenY);
if (Math.abs(_this2._lastScreenY) < _this2.props.distanceToRefresh) {
if (_this2.state.currSt !== 'deactivate') {
// console.log('back to the distance');
_this2.setState({ currSt: 'deactivate' });
}
} else {
if (_this2.state.currSt === 'deactivate') {
// console.log('reach to the distance');
_this2.setState({ currSt: 'activate' });
}
}
// https://github.com/ant-design/ant-design-mobile/issues/573#issuecomment-339560829
// iOS UIWebView issue, It seems no problem in WKWebView
if (isWebView && e.changedTouches[0].clientY < 0) {
_this2.onTouchEnd();
}
}
};
_this2.onTouchEnd = function () {
if (_this2.state.dragOnEdge) {
_this2.setState({ dragOnEdge: false });
}
if (_this2.state.currSt === 'activate') {
_this2.setState({ currSt: 'release' });
_this2._timer = setTimeout(function () {
if (!_this2.props.refreshing) {
_this2.setState({ currSt: 'finish' }, function () {
return _this2.reset();
});
}
_this2._timer = undefined;
}, 1000);
_this2.props.onRefresh();
} else {
_this2.reset();
}
};
_this2.reset = function () {
_this2._lastScreenY = 0;
_this2.setContentStyle(0);
};
_this2.setContentStyle = function (ty) {
// todos: Why sometimes do not have `this.contentRef` ?
if (_this2.contentRef) {
setTransform(_this2.contentRef.style, 'translate3d(0px,' + ty + 'px,0)');
}
};
return _this2;
}
_createClass(PullToRefresh, [{
key: 'shouldComponentUpdate',
value: function shouldComponentUpdate(nextProps) {
this.shouldUpdateChildren = this.props.children !== nextProps.children;
return true;
}
}, {
key: 'componentDidUpdate',
value: function componentDidUpdate(prevProps) {
if (prevProps === this.props || prevProps.refreshing === this.props.refreshing) {
return;
}
// triggerPullToRefresh 需要尽可能减少 setState 次数
this.triggerPullToRefresh();
}
}, {
key: 'componentDidMount',
value: function componentDidMount() {
var _this3 = this;
// `getScrollContainer` most likely return React.Node at the next tick. Need setTimeout
setTimeout(function () {
_this3.init(_this3.props.getScrollContainer() || _this3.containerRef);
_this3.triggerPullToRefresh();
_this3._isMounted = true;
});
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
// Should have no setTimeout here!
this.destroy(this.props.getScrollContainer() || this.containerRef);
}
}, {
key: 'render',
value: function render() {
var _this4 = this;
var props = _extends({}, this.props);
delete props.damping;
var className = props.className,
prefixCls = props.prefixCls,
children = props.children,
getScrollContainer = props.getScrollContainer,
direction = props.direction,
onRefresh = props.onRefresh,
refreshing = props.refreshing,
indicator = props.indicator,
distanceToRefresh = props.distanceToRefresh,
restProps = __rest(props, ["className", "prefixCls", "children", "getScrollContainer", "direction", "onRefresh", "refreshing", "indicator", "distanceToRefresh"]);
var renderChildren = React.createElement(StaticRenderer, { shouldUpdate: this.shouldUpdateChildren, render: function render() {
return children;
} });
var renderRefresh = function renderRefresh(cls) {
var cla = classNames(cls, !_this4.state.dragOnEdge && prefixCls + '-transition');
return React.createElement(
'div',
{ className: prefixCls + '-content-wrapper' },
React.createElement(
'div',
{ className: cla, ref: function ref(el) {
return _this4.contentRef = el;
} },
direction === UP ? renderChildren : null,
React.createElement(
'div',
{ className: prefixCls + '-indicator' },
indicator[_this4.state.currSt] || INDICATOR[_this4.state.currSt]
),
direction === DOWN ? renderChildren : null
)
);
};
if (getScrollContainer()) {
return renderRefresh(prefixCls + '-content ' + prefixCls + '-' + direction);
}
return React.createElement(
'div',
_extends({ ref: function ref(el) {
return _this4.containerRef = el;
}, className: classNames(className, prefixCls, prefixCls + '-' + direction) }, restProps),
renderRefresh(prefixCls + '-content')
);
}
}]);
return PullToRefresh;
}(React.Component);
export default PullToRefresh;
PullToRefresh.defaultProps = {
prefixCls: 'rmc-pull-to-refresh',
getScrollContainer: function getScrollContainer() {
return undefined;
},
direction: DOWN,
distanceToRefresh: 25,
damping: 100,
indicator: INDICATOR,
scale: 0.6
};