rmc-list-view
Version:
m-list-view ui component for react
534 lines (479 loc) • 18.7 kB
JavaScript
import _extends from 'babel-runtime/helpers/extends';
import _objectWithoutProperties from 'babel-runtime/helpers/objectWithoutProperties';
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';
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import DOMScroller from 'zscroller';
import classNames from 'classnames';
import { throttle, setTransform, setTransformOrigin } from './util';
// const SCROLLVIEW = 'ScrollViewRef';
// const INNERVIEW = 'InnerScrollViewRef';
// https://github.com/facebook/react-native/blob/0.26-stable/Libraries/Components/ScrollView/ScrollView.js
/* eslint react/prop-types: 0, react/sort-comp: 0, no-unused-expressions: 0 */
var propTypes = {
children: PropTypes.any,
className: PropTypes.string,
prefixCls: PropTypes.string,
listPrefixCls: PropTypes.string,
listViewPrefixCls: PropTypes.string,
style: PropTypes.object,
contentContainerStyle: PropTypes.object,
onScroll: PropTypes.func,
scrollEventThrottle: PropTypes.number,
refreshControl: PropTypes.element
};
var styles = {
base: {
position: 'relative',
overflow: 'auto',
WebkitOverflowScrolling: 'touch',
flex: 1
},
zScroller: {
position: 'relative',
overflow: 'hidden',
flex: 1
}
};
var ScrollView = function (_React$Component) {
_inherits(ScrollView, _React$Component);
function ScrollView() {
var _ref;
var _temp, _this, _ret;
_classCallCheck(this, ScrollView);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = ScrollView.__proto__ || Object.getPrototypeOf(ScrollView)).call.apply(_ref, [this].concat(args))), _this), _initialiseProps.call(_this), _temp), _possibleConstructorReturn(_this, _ret);
}
_createClass(ScrollView, [{
key: 'componentWillUpdate',
value: function componentWillUpdate(nextProps) {
// https://github.com/ant-design/ant-design-mobile/issues/1480
// https://stackoverflow.com/questions/1386696/make-scrollleft-scrolltop-changes-not-trigger-scroll-event
// 问题情景:用户滚动内容后,改变 dataSource 触发 ListView componentWillReceiveProps
// 内容变化后 scrollTop 如果改变、会自动触发 scroll 事件,而此事件应该避免被执行
if ((this.props.dataSource !== nextProps.dataSource || this.props.initialListSize !== nextProps.initialListSize) && this.tsExec) {
// console.log('componentWillUpdate');
if (this.props.useBodyScroll) {
window.removeEventListener('scroll', this.tsExec);
} else if (!this.props.useZscroller) {
// not handle useZscroller now. todo
ReactDOM.findDOMNode(this.ScrollViewRef).removeEventListener('scroll', this.tsExec);
}
}
}
}, {
key: 'componentDidUpdate',
value: function componentDidUpdate(prevProps) {
var _this2 = this;
// console.log('componentDidUpdate');
if (prevProps.refreshControl && this.props.refreshControl) {
var preRefreshing = prevProps.refreshControl.props.refreshing;
var nowRefreshing = this.props.refreshControl.props.refreshing;
if (preRefreshing && !nowRefreshing && !this._refreshControlTimer) {
this.domScroller.scroller.finishPullToRefresh();
} else if (!this.manuallyRefresh && !preRefreshing && nowRefreshing) {
this.domScroller.scroller.triggerPullToRefresh();
}
}
// handle componentWillUpdate accordingly
if ((this.props.dataSource !== prevProps.dataSource || this.props.initialListSize !== prevProps.initialListSize) && this.tsExec) {
// console.log('componentDidUpdate');
setTimeout(function () {
if (_this2.props.useBodyScroll) {
window.addEventListener('scroll', _this2.tsExec);
} else if (!_this2.props.useZscroller) {
// not handle useZscroller now. todo
ReactDOM.findDOMNode(_this2.ScrollViewRef).addEventListener('scroll', _this2.tsExec);
}
}, 0);
}
// for pullUp
if (this.props.pullUpEnabled) {
var _preRefreshing = prevProps.pullUpRefreshing;
var _nowRefreshing = this.props.pullUpRefreshing;
if (_preRefreshing && !_nowRefreshing && !this._pullUpTimer) {
this.pullUpFinish();
}
}
}
}, {
key: 'componentDidMount',
value: function componentDidMount() {
var _this3 = this;
this.tsExec = this.throttleScroll();
// IE supports onresize on all HTML elements.
// In all other Browsers the onresize is only available at the window object
this.onLayout = function () {
return _this3.props.onLayout({
nativeEvent: { layout: { width: window.innerWidth, height: window.innerHeight } }
});
};
if (this.props.useBodyScroll) {
window.addEventListener('scroll', this.tsExec);
window.addEventListener('resize', this.onLayout);
this.initPullUp(document.body); // for pullUp
} else if (this.props.useZscroller) {
this.renderZscroller();
} else {
var ele = ReactDOM.findDOMNode(this.ScrollViewRef);
ele.addEventListener('scroll', this.tsExec);
this.initPullUp(ele); // for pullUp
}
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
if (this.props.useBodyScroll) {
window.removeEventListener('scroll', this.tsExec);
window.removeEventListener('resize', this.onLayout);
this.destroyPullUp(document.body); // for pullUp
} else if (this.props.useZscroller) {
this.domScroller.destroy();
} else {
var ele = ReactDOM.findDOMNode(this.ScrollViewRef);
ele.removeEventListener('scroll', this.tsExec);
this.destroyPullUp(ele); // for pullUp
}
}
}, {
key: 'renderZscroller',
value: function renderZscroller() {
var _this4 = this;
var _props = this.props,
scrollerOptions = _props.scrollerOptions,
refreshControl = _props.refreshControl;
var _scrollingComplete = scrollerOptions.scrollingComplete,
_onScroll = scrollerOptions.onScroll,
restProps = _objectWithoutProperties(scrollerOptions, ['scrollingComplete', 'onScroll']);
// console.log(scrollingComplete, onScroll, restProps);
// console.log('onRefresh will not change', refreshControl.props.onRefresh.toString());
this.domScroller = new DOMScroller(this.getInnerViewNode(), _extends({
scrollingX: false,
onScroll: function onScroll() {
_this4.tsExec();
if (_onScroll) {
_onScroll();
}
},
scrollingComplete: function scrollingComplete() {
_this4.scrollingComplete();
if (_scrollingComplete) {
_scrollingComplete();
}
}
}, restProps));
if (refreshControl) {
var scroller = this.domScroller.scroller;
var _refreshControl$props = refreshControl.props,
distanceToRefresh = _refreshControl$props.distanceToRefresh,
onRefresh = _refreshControl$props.onRefresh;
scroller.activatePullToRefresh(distanceToRefresh, function () {
// console.log('reach to the distance');
_this4.manuallyRefresh = true;
_this4.overDistanceThenRelease = false;
_this4.RefreshControlRef && _this4.RefreshControlRef.setState({ active: true });
}, function () {
// console.log('back to the distance');
_this4.manuallyRefresh = false;
_this4.RefreshControlRef && _this4.RefreshControlRef.setState({
deactive: _this4.overDistanceThenRelease,
active: false,
loadingState: false
});
}, function () {
// console.log('Over distance and release to loading');
_this4.overDistanceThenRelease = true;
_this4.RefreshControlRef && _this4.RefreshControlRef.setState({
deactive: false,
loadingState: true
});
_this4._refreshControlTimer = setTimeout(function () {
if (!_this4.props.refreshControl.props.refreshing) {
scroller.finishPullToRefresh();
}
_this4._refreshControlTimer = undefined;
}, 1000);
onRefresh();
});
if (refreshControl.props.refreshing) {
scroller.triggerPullToRefresh();
}
}
}
}, {
key: 'render',
value: function render() {
var _this5 = this;
var _props2 = this.props,
children = _props2.children,
className = _props2.className,
prefixCls = _props2.prefixCls,
listPrefixCls = _props2.listPrefixCls,
listViewPrefixCls = _props2.listViewPrefixCls,
_props2$style = _props2.style,
style = _props2$style === undefined ? {} : _props2$style,
_props2$contentContai = _props2.contentContainerStyle,
contentContainerStyle = _props2$contentContai === undefined ? {} : _props2$contentContai,
useZscroller = _props2.useZscroller,
refreshControl = _props2.refreshControl,
useBodyScroll = _props2.useBodyScroll,
pullUpEnabled = _props2.pullUpEnabled,
pullUpRenderer = _props2.pullUpRenderer;
var styleBase = styles.base;
if (useBodyScroll) {
styleBase = {};
} else if (useZscroller) {
styleBase = styles.zScroller;
}
var preCls = prefixCls || listViewPrefixCls || '';
var containerProps = {
ref: function ref(el) {
return _this5.ScrollViewRef = el;
},
style: _extends({}, styleBase, style),
className: classNames(className, preCls + '-scrollview')
};
var contentContainerProps = {
ref: function ref(el) {
return _this5.InnerScrollViewRef = el;
},
style: _extends({ position: 'absolute', minWidth: '100%' }, contentContainerStyle),
className: classNames(preCls + '-scrollview-content', listPrefixCls)
};
if (refreshControl) {
return React.createElement(
'div',
containerProps,
React.createElement(
'div',
contentContainerProps,
React.cloneElement(refreshControl, { ref: function ref(el) {
return _this5.RefreshControlRef = el;
} }),
children
)
);
}
var createPullUp = function createPullUp() {
var pullUpCls = classNames(preCls + '-pull-up-content', !_this5.state.isTouching && preCls + '-pull-up-dropped');
var defaultRenderer = _this5.pullUpDisplay.deactivate;
switch (_this5.state.pullUp) {
case 'activate':
case 'deactivate':
case 'release':
case 'finish':
default:
defaultRenderer = _this5.pullUpDisplay[_this5.state.pullUp];
}
return React.createElement(
'div',
{ className: pullUpCls, ref: function ref(el) {
return _this5.pullUpContentRef = el;
} },
children,
React.createElement(
'div',
{ ref: function ref(el) {
return _this5.pullUpIndicatorRef = el;
}, className: preCls + '-pull-up-indicator' },
pullUpRenderer ? pullUpRenderer(_this5.state.pullUp) : defaultRenderer
)
);
};
if (useBodyScroll) {
if (pullUpEnabled) {
containerProps.style.overflow = 'hidden';
return React.createElement(
'div',
containerProps,
createPullUp()
);
}
return React.createElement(
'div',
containerProps,
children
);
}
if (pullUpEnabled) {
contentContainerProps.style.overflow = 'hidden';
return React.createElement(
'div',
containerProps,
React.createElement(
'div',
contentContainerProps,
createPullUp()
)
);
}
return React.createElement(
'div',
containerProps,
React.createElement(
'div',
contentContainerProps,
children
)
);
}
/**
The following code was intended to implement the pull-up-to-load-more feature,
Coincidentally, it solves a problem, if the content is not high enough,
the `onScroll` and `onEndReached` event will not be fired.
However, there should be a better solution for this issue.
*/
// https://github.com/yiminghe/zscroller/blob/2d97973287135745818a0537712235a39a6a62a1/src/Scroller.js#L355
// states: `activate` / `deactivate` / `release` / `finish`
}]);
return ScrollView;
}(React.Component);
ScrollView.propTypes = propTypes;
var _initialiseProps = function _initialiseProps() {
var _this6 = this;
this.getInnerViewNode = function () {
return ReactDOM.findDOMNode(_this6.InnerScrollViewRef);
};
this.scrollTo = function () {
if (_this6.props.useBodyScroll) {
var _window;
(_window = window).scrollTo.apply(_window, arguments);
} else if (_this6.props.useZscroller) {
var _domScroller$scroller;
// it will change zScroller's dimensions on data loaded, so it needs fire reflow.
_this6.domScroller.reflow();
(_domScroller$scroller = _this6.domScroller.scroller).scrollTo.apply(_domScroller$scroller, arguments);
} else {
var ele = ReactDOM.findDOMNode(_this6.ScrollViewRef);
ele.scrollLeft = arguments.length <= 0 ? undefined : arguments[0];
ele.scrollTop = arguments.length <= 1 ? undefined : arguments[1];
}
};
this.throttleScroll = function () {
var handleScroll = function handleScroll() {};
if (_this6.props.scrollEventThrottle && _this6.props.onScroll) {
handleScroll = throttle(function (e) {
_this6.props.onScroll && _this6.props.onScroll(e);
}, _this6.props.scrollEventThrottle);
}
return handleScroll;
};
this.scrollingComplete = function () {
// console.log('scrolling complete');
if (_this6.props.refreshControl && _this6.RefreshControlRef && _this6.RefreshControlRef.state.deactive) {
_this6.RefreshControlRef.setState({ deactive: false });
}
};
this.state = {
pullUp: false,
isTouching: false
};
this.pullUpStats = {
activate: 'activate',
deactivate: 'deactivate',
release: 'release',
finish: 'finish'
};
this.pullUpDisplay = {
activate: '释放刷新',
deactivate: '上拉 ↑',
release: '加载中...',
finish: '完成刷新'
};
this.genEvtHandler = function (ele) {
return {
touchstart: _this6.onTouchStart.bind(_this6, ele),
touchmove: _this6.onTouchMove.bind(_this6, ele),
touchend: _this6.onTouchEnd.bind(_this6, ele),
touchcancel: _this6.onTouchEnd.bind(_this6, ele)
};
};
this.initPullUp = function (ele) {
if (_this6.pullUpContentRef) {
setTransformOrigin(_this6.pullUpContentRef.style, 'left top');
}
_this6._to = _this6.genEvtHandler(ele);
Object.keys(_this6._to).forEach(function (key) {
ele.addEventListener(key, _this6._to[key]);
});
};
this.destroyPullUp = function (ele) {
Object.keys(_this6._to).forEach(function (key) {
ele.removeEventListener(key, _this6._to[key]);
});
};
this.onTouchStart = function (ele, e) {
_this6._pullUpScreenY = _this6._pullUpStartScreenY = e.touches[0].screenY;
_this6._pullUpLastScreenY = 0;
if (_this6.props.pullUpEnabled) {
_this6.setState({ isTouching: true });
}
};
this.onTouchMove = function (ele, e) {
if (!_this6.props.pullUpEnabled) {
return;
}
// 使用 pageY 对比有问题
var _screenY = e.touches[0].screenY;
if (_this6._pullUpStartScreenY - _screenY > 0) {
// console.log('is pull up', _screenY);
var isReachBottom = void 0;
if (_this6.props.useBodyScroll) {
// In chrome61 `document.body.scrollTop` is invalid, here `ele === document.body`
var scrollNode = document.scrollingElement ? document.scrollingElement : ele;
isReachBottom = ele.scrollHeight - scrollNode.scrollTop <= window.innerHeight;
// console.log(ele.scrollHeight, scrollNode.scrollTop, window.innerHeight);
} else {
isReachBottom = ele.scrollHeight - ele.scrollTop === ele.clientHeight;
}
if (isReachBottom) {
var _diff = Math.round(_screenY - _this6._pullUpScreenY);
_this6._pullUpScreenY = _screenY;
_this6._pullUpLastScreenY += _diff;
setTransform(_this6.pullUpContentRef.style, 'translate3d(0px,' + _this6._pullUpLastScreenY + 'px,0)');
if (Math.abs(_this6._pullUpLastScreenY) < _this6.props.pullUpDistance) {
if (_this6.state.pullUp !== _this6.pullUpStats.deactivate) {
// console.log('back to the distance');
_this6.setState({ pullUp: _this6.pullUpStats.deactivate });
}
} else {
if (_this6.state.pullUp === _this6.pullUpStats.deactivate) {
// console.log('reach to the distance');
_this6.setState({ pullUp: _this6.pullUpStats.activate });
}
}
}
}
};
this.onTouchEnd = function () {
if (_this6.props.pullUpEnabled) {
_this6.setState({ isTouching: false });
}
if (_this6.state.pullUp === _this6.pullUpStats.deactivate) {
_this6.pullUpFinish();
} else if (_this6.state.pullUp === _this6.pullUpStats.activate) {
_this6.setState({ pullUp: _this6.pullUpStats.release });
_this6._pullUpTimer = setTimeout(function () {
if (!_this6.props.pullUpRefreshing) {
_this6.pullUpFinish();
}
_this6._pullUpTimer = undefined;
}, 1000);
_this6.props.pullUpOnRefresh();
}
};
this.pullUpFinish = function () {
_this6._pullUpLastScreenY = 0;
setTransform(_this6.pullUpContentRef.style, 'translate3d(0px,0px,0)');
if (_this6.state.pullUp === _this6.pullUpStats.release) {
_this6.setState({ pullUp: _this6.pullUpStats.finish });
}
};
};
export default ScrollView;