rmc-list-view
Version:
m-list-view ui component for react
575 lines (477 loc) • 22.3 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _extends2 = require('babel-runtime/helpers/extends');
var _extends3 = _interopRequireDefault(_extends2);
var _objectWithoutProperties2 = require('babel-runtime/helpers/objectWithoutProperties');
var _objectWithoutProperties3 = _interopRequireDefault(_objectWithoutProperties2);
var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn');
var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2);
var _inherits2 = require('babel-runtime/helpers/inherits');
var _inherits3 = _interopRequireDefault(_inherits2);
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _reactDom = require('react-dom');
var _reactDom2 = _interopRequireDefault(_reactDom);
var _ListViewDataSource = require('./ListViewDataSource');
var _ListViewDataSource2 = _interopRequireDefault(_ListViewDataSource);
var _ScrollView = require('./ScrollView');
var _ScrollView2 = _interopRequireDefault(_ScrollView);
var _ScrollResponder = require('./ScrollResponder');
var _ScrollResponder2 = _interopRequireDefault(_ScrollResponder);
var _StaticRenderer = require('./StaticRenderer');
var _StaticRenderer2 = _interopRequireDefault(_StaticRenderer);
var _reactTimerMixin = require('react-timer-mixin');
var _reactTimerMixin2 = _interopRequireDefault(_reactTimerMixin);
var _objectAssign = require('object-assign');
var _objectAssign2 = _interopRequireDefault(_objectAssign);
var _reactMixin = require('react-mixin');
var _reactMixin2 = _interopRequireDefault(_reactMixin);
var _autobindDecorator = require('autobind-decorator');
var _autobindDecorator2 = _interopRequireDefault(_autobindDecorator);
var _reactSticky = require('react-sticky');
var _util = require('./util');
var _PullUpLoadMoreMixin = require('./PullUpLoadMoreMixin');
var _PullUpLoadMoreMixin2 = _interopRequireDefault(_PullUpLoadMoreMixin);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
var DEFAULT_PAGE_SIZE = 1; // https://github.com/facebook/react-native/blob/master/Libraries/CustomComponents/ListView/ListView.js
var DEFAULT_INITIAL_ROWS = 10;
var DEFAULT_SCROLL_RENDER_AHEAD = 1000;
var DEFAULT_END_REACHED_THRESHOLD = 1000;
var DEFAULT_SCROLL_CALLBACK_THROTTLE = 50;
var SCROLLVIEW_REF = 'listviewscroll';
var ListView = function (_React$Component) {
(0, _inherits3["default"])(ListView, _React$Component);
function ListView() {
var _temp, _this, _ret;
(0, _classCallCheck3["default"])(this, ListView);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _ret = (_temp = (_this = (0, _possibleConstructorReturn3["default"])(this, _React$Component.call.apply(_React$Component, [this].concat(args))), _this), _this.state = {
curRenderedRowsCount: _this.props.initialListSize,
highlightedRow: {}
}, _this.stickyRefs = {}, _temp), (0, _possibleConstructorReturn3["default"])(_this, _ret);
}
/**
* React life cycle hooks.
*/
/**
* Exports some data, e.g. for perf investigations or analytics.
*/
ListView.prototype.getMetrics = function getMetrics() {
return {
contentLength: this.scrollProperties.contentLength,
totalRows: this.props.dataSource.getRowCount(),
renderedRows: this.state.curRenderedRowsCount,
visibleRows: Object.keys(this._visibleRows).length
};
};
/**
* Provides a handle to the underlying scroll responder.
* Note that the view in `SCROLLVIEW_REF` may not be a `ScrollView`, so we
* need to check that it responds to `getScrollResponder` before calling it.
*/
ListView.prototype.getScrollResponder = function getScrollResponder() {
return this.refs[SCROLLVIEW_REF] && this.refs[SCROLLVIEW_REF].getScrollResponder && this.refs[SCROLLVIEW_REF].getScrollResponder();
};
ListView.prototype.scrollTo = function scrollTo() {
var _refs$SCROLLVIEW_REF;
this.refs[SCROLLVIEW_REF] && this.refs[SCROLLVIEW_REF].scrollTo && (_refs$SCROLLVIEW_REF = this.refs[SCROLLVIEW_REF]).scrollTo.apply(_refs$SCROLLVIEW_REF, arguments);
};
ListView.prototype.setNativeProps = function setNativeProps(props) {
this.refs[SCROLLVIEW_REF] && this.refs[SCROLLVIEW_REF].setNativeProps(props);
};
ListView.prototype.getInnerViewNode = function getInnerViewNode() {
return this.refs[SCROLLVIEW_REF].getInnerViewNode();
};
ListView.prototype.componentWillMount = function componentWillMount() {
// this data should never trigger a render pass, so don't put in state
this.scrollProperties = {
visibleLength: null,
contentLength: null,
offset: 0
};
this._childFrames = [];
this._visibleRows = {};
this._prevRenderedRowsCount = 0;
this._sentEndForContentLength = null;
};
ListView.prototype.componentDidMount = function componentDidMount() {
// do this in animation frame until componentDidMount actually runs after
// the component is laid out
// this.requestAnimationFrame(() => {
// this._measureAndUpdateScrollProps();
// });
if (this.props.stickyHeader || this.props.useBodyScroll) {
this.__onScroll = (0, _util.throttle)(this._onScroll, this.props.scrollEventThrottle);
window.addEventListener('scroll', this.__onScroll);
}
};
ListView.prototype.componentWillReceiveProps = function componentWillReceiveProps(nextProps) {
var _this2 = this;
if (this.props.dataSource !== nextProps.dataSource || this.props.initialListSize !== nextProps.initialListSize) {
this.setState(function (state, props) {
_this2._prevRenderedRowsCount = 0;
return {
curRenderedRowsCount: Math.min(Math.max(state.curRenderedRowsCount, props.initialListSize), props.dataSource.getRowCount())
};
}, function () {
return _this2._renderMoreRowsIfNeeded();
});
}
};
ListView.prototype.componentWillUnmount = function componentWillUnmount() {
if (this.props.stickyHeader || this.props.useBodyScroll) {
window.removeEventListener('scroll', this.__onScroll);
}
};
ListView.prototype.onRowHighlighted = function onRowHighlighted(sectionID, rowID) {
this.setState({ highlightedRow: { sectionID: sectionID, rowID: rowID } });
};
ListView.prototype.render = function render() {
var _this3 = this;
var bodyComponents = [];
var dataSource = this.props.dataSource;
var allRowIDs = dataSource.rowIdentities;
var rowCount = 0;
var sectionHeaderIndices = [];
var header = this.props.renderHeader && this.props.renderHeader();
var footer = this.props.renderFooter && this.props.renderFooter();
var totalIndex = header ? 1 : 0;
var _loop = function _loop(sectionIdx) {
var sectionID = dataSource.sectionIdentities[sectionIdx];
var rowIDs = allRowIDs[sectionIdx];
if (rowIDs.length === 0) {
return 'continue';
}
if (_this3.props.renderSectionHeader) {
var shouldUpdateHeader = rowCount >= _this3._prevRenderedRowsCount && dataSource.sectionHeaderShouldUpdate(sectionIdx);
var renderSectionHeader = _react2["default"].createElement(_StaticRenderer2["default"], {
key: 's_' + sectionID,
shouldUpdate: !!shouldUpdateHeader,
render: _this3.props.renderSectionHeader.bind(null, dataSource.getSectionHeaderData(sectionIdx), sectionID)
});
if (_this3.props.stickyHeader) {
renderSectionHeader = _react2["default"].createElement(
_reactSticky.Sticky,
(0, _extends3["default"])({}, _this3.props.stickyProps, { key: 's_' + sectionID,
ref: function ref(c) {
_this3.stickyRefs[sectionID] = c;
}
}),
renderSectionHeader
);
}
bodyComponents.push(renderSectionHeader);
sectionHeaderIndices.push(totalIndex++);
}
var sectionBody = [];
for (var rowIdx = 0; rowIdx < rowIDs.length; rowIdx++) {
var rowID = rowIDs[rowIdx];
var comboID = sectionID + '_' + rowID;
var shouldUpdateRow = rowCount >= _this3._prevRenderedRowsCount && dataSource.rowShouldUpdate(sectionIdx, rowIdx);
var row = _react2["default"].createElement(_StaticRenderer2["default"], {
key: 'r_' + comboID,
shouldUpdate: !!shouldUpdateRow,
render: _this3.props.renderRow.bind(null, dataSource.getRowData(sectionIdx, rowIdx), sectionID, rowID, _this3.onRowHighlighted)
});
// bodyComponents.push(row);
sectionBody.push(row);
totalIndex++;
if (_this3.props.renderSeparator && (rowIdx !== rowIDs.length - 1 || sectionIdx === allRowIDs.length - 1)) {
var adjacentRowHighlighted = _this3.state.highlightedRow.sectionID === sectionID && (_this3.state.highlightedRow.rowID === rowID || _this3.state.highlightedRow.rowID === rowIDs[rowIdx + 1]);
var separator = _this3.props.renderSeparator(sectionID, rowID, adjacentRowHighlighted);
if (separator) {
// bodyComponents.push(separator);
sectionBody.push(separator);
totalIndex++;
}
}
if (++rowCount === _this3.state.curRenderedRowsCount) {
break;
}
}
bodyComponents.push(_react2["default"].cloneElement(_this3.props.renderSectionBodyWrapper(sectionID), {
className: _this3.props.sectionBodyClassName
}, sectionBody));
if (rowCount >= _this3.state.curRenderedRowsCount) {
return 'break';
}
};
_loop2: for (var sectionIdx = 0; sectionIdx < allRowIDs.length; sectionIdx++) {
var _ret2 = _loop(sectionIdx);
switch (_ret2) {
case 'continue':
continue;
case 'break':
break _loop2;}
}
var _props = this.props,
renderScrollComponent = _props.renderScrollComponent,
props = (0, _objectWithoutProperties3["default"])(_props, ['renderScrollComponent']);
bodyComponents = _react2["default"].cloneElement(props.renderBodyComponent(), {}, bodyComponents);
if (props.stickyHeader) {
bodyComponents = _react2["default"].createElement(
_reactSticky.StickyContainer,
props.stickyContainerProps,
bodyComponents
);
}
(0, _objectAssign2["default"])(props, {
onScroll: this._onScroll
});
if (props.stickyHeader || props.useBodyScroll) {
delete props.onScroll;
}
this._sc = _react2["default"].cloneElement(renderScrollComponent(props), {
ref: SCROLLVIEW_REF,
onContentSizeChange: this._onContentSizeChange,
onLayout: props.stickyHeader || props.useBodyScroll ? function (event) {
_this3.props.onLayout && _this3.props.onLayout(event);
} : this._onLayout
}, header, bodyComponents, footer, props.children);
return this._sc;
};
/**
* Private methods
*/
ListView.prototype._measureAndUpdateScrollProps = function _measureAndUpdateScrollProps() {
var scrollComponent = this.getScrollResponder();
if (!scrollComponent || !scrollComponent.getInnerViewNode) {
return;
}
// RCTScrollViewManager.calculateChildFrames is not available on
// every platform
// RCTScrollViewManager && RCTScrollViewManager.calculateChildFrames &&
// RCTScrollViewManager.calculateChildFrames(
// React.findNodeHandle(scrollComponent),
// this._updateVisibleRows,
// );
};
ListView.prototype._onContentSizeChange = function _onContentSizeChange(width, height) {
var contentLength = !this.props.horizontal ? height : width;
if (contentLength !== this.scrollProperties.contentLength) {
this.scrollProperties.contentLength = contentLength;
this._updateVisibleRows();
this._renderMoreRowsIfNeeded();
}
this.props.onContentSizeChange && this.props.onContentSizeChange(width, height);
};
ListView.prototype._onLayout = function _onLayout(event) {
var _event$nativeEvent$la = event.nativeEvent.layout,
width = _event$nativeEvent$la.width,
height = _event$nativeEvent$la.height;
var visibleLength = !this.props.horizontal ? height : width;
if (visibleLength !== this.scrollProperties.visibleLength) {
this.scrollProperties.visibleLength = visibleLength;
this._updateVisibleRows();
this._renderMoreRowsIfNeeded();
}
this.props.onLayout && this.props.onLayout(event);
};
ListView.prototype._maybeCallOnEndReached = function _maybeCallOnEndReached(event) {
// console.log(this.scrollProperties, this._getDistanceFromEnd(this.scrollProperties));
if (this.props.onEndReached &&
// this.scrollProperties.contentLength !== this._sentEndForContentLength &&
this._getDistanceFromEnd(this.scrollProperties) < this.props.onEndReachedThreshold && this.state.curRenderedRowsCount === this.props.dataSource.getRowCount()) {
this._sentEndForContentLength = this.scrollProperties.contentLength;
this.props.onEndReached(event);
return true;
}
return false;
};
ListView.prototype._renderMoreRowsIfNeeded = function _renderMoreRowsIfNeeded() {
if (this.scrollProperties.contentLength === null || this.scrollProperties.visibleLength === null || this.state.curRenderedRowsCount === this.props.dataSource.getRowCount()) {
this._maybeCallOnEndReached();
return;
}
var distanceFromEnd = this._getDistanceFromEnd(this.scrollProperties);
// console.log(distanceFromEnd, this.props.scrollRenderAheadDistance);
if (distanceFromEnd < this.props.scrollRenderAheadDistance) {
this._pageInNewRows();
}
};
ListView.prototype._pageInNewRows = function _pageInNewRows() {
var _this4 = this;
this.setState(function (state, props) {
var rowsToRender = Math.min(state.curRenderedRowsCount + props.pageSize, props.dataSource.getRowCount());
_this4._prevRenderedRowsCount = state.curRenderedRowsCount;
return {
curRenderedRowsCount: rowsToRender
};
}, function () {
_this4._measureAndUpdateScrollProps();
_this4._prevRenderedRowsCount = _this4.state.curRenderedRowsCount;
});
};
ListView.prototype._getDistanceFromEnd = function _getDistanceFromEnd(scrollProperties) {
return scrollProperties.contentLength - scrollProperties.visibleLength - scrollProperties.offset;
};
ListView.prototype._updateVisibleRows = function _updateVisibleRows(updatedFrames) {
// if (!this.props.onChangeVisibleRows) {
// return; // No need to compute visible rows if there is no callback
// }
// if (updatedFrames) {
// updatedFrames.forEach((newFrame) => {
// this._childFrames[newFrame.index] = merge(newFrame);
// });
// }
// let isVertical = !this.props.horizontal;
// let dataSource = this.props.dataSource;
// let visibleMin = this.scrollProperties.offset;
// let visibleMax = visibleMin + this.scrollProperties.visibleLength;
// let allRowIDs = dataSource.rowIdentities;
//
// let header = this.props.renderHeader && this.props.renderHeader();
// let totalIndex = header ? 1 : 0;
// let visibilityChanged = false;
// let changedRows = {};
// for (let sectionIdx = 0; sectionIdx < allRowIDs.length; sectionIdx++) {
// let rowIDs = allRowIDs[sectionIdx];
// if (rowIDs.length === 0) {
// continue;
// }
// let sectionID = dataSource.sectionIdentities[sectionIdx];
// if (this.props.renderSectionHeader) {
// totalIndex++;
// }
// let visibleSection = this._visibleRows[sectionID];
// if (!visibleSection) {
// visibleSection = {};
// }
// for (let rowIdx = 0; rowIdx < rowIDs.length; rowIdx++) {
// let rowID = rowIDs[rowIdx];
// let frame = this._childFrames[totalIndex];
// totalIndex++;
// if (!frame) {
// break;
// }
// let rowVisible = visibleSection[rowID];
// let min = isVertical ? frame.y : frame.x;
// let max = min + (isVertical ? frame.height : frame.width);
// if (min > visibleMax || max < visibleMin) {
// if (rowVisible) {
// visibilityChanged = true;
// delete visibleSection[rowID];
// if (!changedRows[sectionID]) {
// changedRows[sectionID] = {};
// }
// changedRows[sectionID][rowID] = false;
// }
// } else if (!rowVisible) {
// visibilityChanged = true;
// visibleSection[rowID] = true;
// if (!changedRows[sectionID]) {
// changedRows[sectionID] = {};
// }
// changedRows[sectionID][rowID] = true;
// }
// }
// if (!isEmpty(visibleSection)) {
// this._visibleRows[sectionID] = visibleSection;
// } else if (this._visibleRows[sectionID]) {
// delete this._visibleRows[sectionID];
// }
// }
// visibilityChanged && this.props.onChangeVisibleRows(this._visibleRows, changedRows);
};
ListView.prototype._onScroll = function _onScroll(e) {
var isVertical = !this.props.horizontal;
// this.scrollProperties.visibleLength = e.nativeEvent.layoutMeasurement[
// isVertical ? 'height' : 'width'
// ];
// this.scrollProperties.contentLength = e.nativeEvent.contentSize[
// isVertical ? 'height' : 'width'
// ];
// this.scrollProperties.offset = e.nativeEvent.contentOffset[
// isVertical ? 'y' : 'x'
// ];
var ev = e;
var target = _reactDom2["default"].findDOMNode(this.refs[SCROLLVIEW_REF]);
if (this.props.stickyHeader || this.props.useBodyScroll) {
this.scrollProperties.visibleLength = window[isVertical ? 'innerHeight' : 'innerWidth'];
this.scrollProperties.contentLength = target[isVertical ? 'scrollHeight' : 'scrollWidth'];
this.scrollProperties.offset = window.document.body[isVertical ? 'scrollTop' : 'scrollLeft'];
} else if (this.props.useZscroller) {
var domScroller = this.refs[SCROLLVIEW_REF].domScroller;
ev = domScroller;
this.scrollProperties.visibleLength = domScroller.container[isVertical ? 'clientHeight' : 'clientWidth'];
this.scrollProperties.contentLength = domScroller.content[isVertical ? 'offsetHeight' : 'offsetWidth'];
this.scrollProperties.offset = domScroller.scroller.getValues()[isVertical ? 'top' : 'left'];
// console.log(this.scrollProperties, domScroller.scroller.getScrollMax())
} else {
this.scrollProperties.visibleLength = target[isVertical ? 'offsetHeight' : 'offsetWidth'];
this.scrollProperties.contentLength = target[isVertical ? 'scrollHeight' : 'scrollWidth'];
this.scrollProperties.offset = target[isVertical ? 'scrollTop' : 'scrollLeft'];
}
// this._updateVisibleRows(e.nativeEvent.updatedChildFrames);
if (!this._maybeCallOnEndReached(ev)) {
this._renderMoreRowsIfNeeded();
}
if (this.props.onEndReached && this._getDistanceFromEnd(this.scrollProperties) > this.props.onEndReachedThreshold) {
// Scrolled out of the end zone, so it should be able to trigger again.
this._sentEndForContentLength = null;
}
this.props.onScroll && this.props.onScroll(ev);
};
return ListView;
}(_react2["default"].Component);
ListView.DataSource = _ListViewDataSource2["default"];
ListView.propTypes = (0, _extends3["default"])({}, _ScrollView2["default"].propTypes, {
dataSource: _react.PropTypes.instanceOf(_ListViewDataSource2["default"]).isRequired,
renderSeparator: _react.PropTypes.func,
renderRow: _react.PropTypes.func.isRequired,
initialListSize: _react.PropTypes.number,
onEndReached: _react.PropTypes.func,
onEndReachedThreshold: _react.PropTypes.number,
pageSize: _react.PropTypes.number,
renderFooter: _react.PropTypes.func,
renderHeader: _react.PropTypes.func,
renderSectionHeader: _react.PropTypes.func,
renderScrollComponent: _react2["default"].PropTypes.func.isRequired,
scrollRenderAheadDistance: _react2["default"].PropTypes.number,
onChangeVisibleRows: _react2["default"].PropTypes.func,
scrollEventThrottle: _react2["default"].PropTypes.number,
// removeClippedSubviews: React.PropTypes.bool,
// stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number),
// another added
renderBodyComponent: _react.PropTypes.func,
renderSectionBodyWrapper: _react.PropTypes.func,
sectionBodyClassName: _react.PropTypes.string,
useZscroller: _react.PropTypes.bool, // for web
useBodyScroll: _react.PropTypes.bool, // for web
stickyHeader: _react.PropTypes.bool, // for web
stickyProps: _react.PropTypes.object, // https://github.com/captivationsoftware/react-sticky/blob/master/README.md#sticky--props
stickyContainerProps: _react.PropTypes.object
});
ListView.defaultProps = {
initialListSize: DEFAULT_INITIAL_ROWS,
pageSize: DEFAULT_PAGE_SIZE,
renderScrollComponent: function renderScrollComponent(props) {
return _react2["default"].createElement(_ScrollView2["default"], props);
},
renderBodyComponent: function renderBodyComponent() {
return _react2["default"].createElement('div', null);
},
renderSectionBodyWrapper: function renderSectionBodyWrapper(sectionID) {
return _react2["default"].createElement('div', { key: sectionID });
},
sectionBodyClassName: 'list-view-section-body',
scrollRenderAheadDistance: DEFAULT_SCROLL_RENDER_AHEAD,
onEndReachedThreshold: DEFAULT_END_REACHED_THRESHOLD,
scrollEventThrottle: DEFAULT_SCROLL_CALLBACK_THROTTLE,
// stickyHeaderIndices: [],
stickyProps: {},
stickyContainerProps: {}
};
(0, _reactMixin2["default"])(ListView.prototype, _ScrollResponder2["default"].Mixin);
(0, _reactMixin2["default"])(ListView.prototype, _reactTimerMixin2["default"]);
(0, _reactMixin2["default"])(ListView.prototype, _PullUpLoadMoreMixin2["default"]);
(0, _autobindDecorator2["default"])(ListView);
ListView.isReactNativeComponent = true;
exports["default"] = ListView;
module.exports = exports['default'];