react-native-swipe-list-view
Version:
A ListView with rows that swipe open and closed.
719 lines (687 loc) • 26.1 kB
JavaScript
'use strict';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Animated, FlatList, Platform, SectionList } from 'react-native';
import SwipeRow from './SwipeRow';
/**
* ListView that renders SwipeRows.
*/
class SwipeListView extends PureComponent {
constructor(props) {
super(props);
this._rows = {};
this.openCellKey = null;
this.listViewProps = {};
if (Platform.OS === 'ios') {
// Keep track of scroll offset and layout changes on iOS to be able to handle
// https://github.com/jemise111/react-native-swipe-list-view/issues/109
this.yScrollOffset = 0;
this.layoutHeight = 0;
this.listViewProps = {
onLayout: e => this.onLayout(e),
onContentSizeChange: (w, h) => this.onContentSizeChange(w, h),
};
}
this._onScroll = this.onScroll.bind(this);
if (this.props.onScroll && typeof this.props.onScroll === 'object') {
// Animated.event
this.props.onScroll.__addListener(this._onScroll);
this._onScroll = this.props.onScroll;
}
}
componentDidUpdate() {
if (this.props.refreshing) {
this.safeCloseOpenRow();
}
}
setScrollEnabled(enable) {
if (this.props.scrollEnabled === false) {
return;
}
// Due to multiple issues reported across different versions of RN
// We do this in the safest way possible...
if (this._listView && this._listView.setNativeProps) {
this._listView.setNativeProps({ scrollEnabled: enable });
} else if (this._listView && this._listView.getScrollResponder) {
const scrollResponder = this._listView.getScrollResponder();
scrollResponder.setNativeProps &&
scrollResponder.setNativeProps({ scrollEnabled: enable });
}
this.props.onScrollEnabled && this.props.onScrollEnabled(enable);
}
safeCloseOpenRow() {
const rowRef = this._rows[this.openCellKey];
if (rowRef && rowRef.closeRow) {
this._rows[this.openCellKey].closeRow();
}
}
rowSwipeGestureBegan(key) {
if (
this.props.closeOnRowBeginSwipe &&
this.openCellKey &&
this.openCellKey !== key
) {
this.safeCloseOpenRow();
}
if (this.props.swipeGestureBegan) {
this.props.swipeGestureBegan(key);
}
}
rowSwipeGestureEnded(key, data) {
if (this.props.swipeGestureEnded) {
this.props.swipeGestureEnded(key, data);
}
}
onRowOpen(key, toValue) {
if (
this.openCellKey &&
this.openCellKey !== key &&
this.props.closeOnRowOpen &&
!this.props.closeOnRowBeginSwipe
) {
this.safeCloseOpenRow();
}
this.openCellKey = key;
this.props.onRowOpen && this.props.onRowOpen(key, this._rows, toValue);
}
onRowPress() {
if (this.openCellKey) {
if (this.props.closeOnRowPress) {
this.safeCloseOpenRow();
this.openCellKey = null;
}
}
}
onScroll(e) {
if (Platform.OS === 'ios') {
this.yScrollOffset = e.nativeEvent.contentOffset.y;
}
if (this.openCellKey) {
if (this.props.closeOnScroll) {
this.safeCloseOpenRow();
this.openCellKey = null;
}
}
typeof this.props.onScroll === 'function' && this.props.onScroll(e);
}
onLayout(e) {
this.layoutHeight = e.nativeEvent.layout.height;
this.props.onLayout && this.props.onLayout(e);
}
// When deleting rows on iOS, the list may end up being over-scrolled,
// which will prevent swiping any of the remaining rows. This triggers a scrollToEnd
// when that happens, which will make sure the list is kept in bounds.
// See: https://github.com/jemise111/react-native-swipe-list-view/issues/109
onContentSizeChange(w, h) {
const height = h - this.layoutHeight;
if (this.yScrollOffset >= height && height > 0) {
if (this._listView instanceof FlatList) {
this._listView && this._listView.scrollToEnd();
} else if (this._listView instanceof SectionList) {
this._listView.scrollToEnd && this._listView.scrollToEnd();
} else if (this._listView instanceof Animated.FlatList) {
this._listView.scrollToEnd && this._listView.scrollToEnd();
}
}
this.props.onContentSizeChange && this.props.onContentSizeChange(w, h);
}
setRefs(ref) {
this._listView = ref;
if (typeof this.props.listViewRef === 'function') {
this.props.listViewRef && this.props.listViewRef(ref);
} else if (typeof this.props.listViewRef === 'object') {
if (Object.keys(this.props.listViewRef).includes('current')) {
this.props.listViewRef.current = ref;
}
}
}
closeAllOpenRows() {
Object.keys(this._rows).forEach(rowKey => {
const row = this._rows[rowKey];
if (row) {
const rowTranslateX = Math.round(row.currentTranslateX || 0);
if (row.closeRow && rowTranslateX !== 0) {
row.closeRow();
}
}
});
}
manuallyOpenAllRows(toValue) {
Object.keys(this._rows).forEach(rowKey => {
const row = this._rows[rowKey];
if (row && row.manuallySwipeRow) {
row.manuallySwipeRow(toValue);
}
});
}
renderCell(VisibleComponent, HiddenComponent, key, item, shouldPreviewRow) {
if (!HiddenComponent) {
return React.cloneElement(VisibleComponent, {
...VisibleComponent.props,
ref: row => (this._rows[key] = row),
onRowOpen: toValue => this.onRowOpen(key, toValue),
onRowDidOpen: toValue =>
this.props.onRowDidOpen &&
this.props.onRowDidOpen(key, this._rows, toValue),
onRowClose: () =>
this.props.onRowClose &&
this.props.onRowClose(key, this._rows),
onRowDidClose: () =>
this.props.onRowDidClose &&
this.props.onRowDidClose(key, this._rows),
onRowPress: () => this.onRowPress(),
setScrollEnabled: enable => this.setScrollEnabled(enable),
swipeGestureBegan: () => this.rowSwipeGestureBegan(key),
swipeGestureEnded: (_, data) =>
this.rowSwipeGestureEnded(key, data),
});
} else {
return (
<SwipeRow
onSwipeValueChange={
this.props.onSwipeValueChange
? data =>
this.props.onSwipeValueChange({
...data,
key,
})
: null
}
ref={row => (this._rows[key] = row)}
swipeGestureBegan={() => this.rowSwipeGestureBegan(key)}
swipeGestureEnded={(_, data) =>
this.rowSwipeGestureEnded(key, data)
}
onRowOpen={toValue => this.onRowOpen(key, toValue)}
onRowDidOpen={toValue =>
this.props.onRowDidOpen &&
this.props.onRowDidOpen(key, this._rows, toValue)
}
onRowClose={() =>
this.props.onRowClose &&
this.props.onRowClose(key, this._rows)
}
onRowDidClose={() =>
this.props.onRowDidClose &&
this.props.onRowDidClose(key, this._rows)
}
onRowPress={() => this.onRowPress(key)}
leftActivationValue={
item.leftActivationValue ||
this.props.leftActivationValue
}
rightActivationValue={
item.rightActivationValue ||
this.props.rightActivationValue
}
leftActionValue={
item.leftActionValue || this.props.leftActionValue || 0
}
rightActionValue={
item.rightActionValue ||
this.props.rightActionValue ||
0
}
initialLeftActionState={
item.initialLeftActionState ||
this.props.initialLeftActionState
}
initialRightActionState={
item.initialRightActionState ||
this.props.initialRightActionState
}
onLeftAction={() =>
item.onLeftAction ||
(this.props.onLeftAction &&
this.props.onLeftAction(key, this._rows))
}
onRightAction={() =>
item.onRightAction ||
(this.props.onRightAction &&
this.props.onRightAction(key, this._rows))
}
onLeftActionStatusChange={
this.props.onLeftActionStatusChange
? data =>
this.props.onLeftActionStatusChange({
...data,
key,
})
: null
}
onRightActionStatusChange={
this.props.onRightActionStatusChange
? data =>
this.props.onRightActionStatusChange({
...data,
key,
})
: null
}
shouldItemUpdate={
this.props.shouldItemUpdate
? (currentItem, newItem) =>
this.props.shouldItemUpdate(
currentItem,
newItem
)
: null
}
setScrollEnabled={enable => this.setScrollEnabled(enable)}
leftOpenValue={
item.leftOpenValue || this.props.leftOpenValue
}
rightOpenValue={
item.rightOpenValue || this.props.rightOpenValue
}
closeOnRowPress={
item.closeOnRowPress || this.props.closeOnRowPress
}
disableLeftSwipe={
item.disableLeftSwipe || this.props.disableLeftSwipe
}
disableRightSwipe={
item.disableRightSwipe || this.props.disableRightSwipe
}
stopLeftSwipe={
item.stopLeftSwipe || this.props.stopLeftSwipe
}
stopRightSwipe={
item.stopRightSwipe || this.props.stopRightSwipe
}
recalculateHiddenLayout={this.props.recalculateHiddenLayout}
disableHiddenLayoutCalculation={
this.props.disableHiddenLayoutCalculation
}
style={this.props.swipeRowStyle}
preview={shouldPreviewRow}
previewDuration={this.props.previewDuration}
previewOpenDelay={this.props.previewOpenDelay}
previewOpenValue={this.props.previewOpenValue}
previewRepeat={this.props.previewRepeat}
previewRepeatDelay={this.props.previewRepeatDelay}
tension={this.props.tension}
restSpeedThreshold={this.props.restSpeedThreshold}
restDisplacementThreshold={
this.props.restDisplacementThreshold
}
friction={this.props.friction}
directionalDistanceChangeThreshold={
this.props.directionalDistanceChangeThreshold
}
swipeToOpenPercent={this.props.swipeToOpenPercent}
swipeToOpenVelocityContribution={
this.props.swipeToOpenVelocityContribution
}
swipeToClosePercent={this.props.swipeToClosePercent}
item={item} // used for should item update comparisons
useNativeDriver={this.props.useNativeDriver}
onPreviewEnd={this.props.onPreviewEnd}
>
{HiddenComponent}
{VisibleComponent}
</SwipeRow>
);
}
}
// In most use cases this is no longer used. Only in the case we are passed renderListView={true}
// there may be legacy code that passes a this.props.renderRow func so we keep this for legacy purposes
renderRow(rowData, secId, rowId, rowMap) {
const key = `${secId}${rowId}`;
const Component = this.props.renderRow(rowData, secId, rowId, rowMap);
const HiddenComponent =
this.props.renderHiddenRow &&
this.props.renderHiddenRow(rowData, secId, rowId, rowMap);
const previewRowId =
this.props.dataSource &&
this.props.dataSource.getRowIDForFlatIndex(
this.props.previewRowIndex || 0
);
const shouldPreviewRow =
(this.props.previewFirstRow || this.props.previewRowIndex) &&
rowId === previewRowId;
return this.renderCell(
Component,
HiddenComponent,
key,
rowData,
shouldPreviewRow
);
}
renderItem(rowData, rowMap) {
const Component = this.props.renderItem(rowData, rowMap);
const HiddenComponent =
this.props.renderHiddenItem &&
this.props.renderHiddenItem(rowData, rowMap);
const { item, index } = rowData;
let { key } = item;
if (this.props.keyExtractor) {
key = this.props.keyExtractor(item, index);
}
const shouldPreviewRow =
typeof key !== 'undefined' && this.props.previewRowKey === key;
return this.renderCell(
Component,
HiddenComponent,
key,
item,
shouldPreviewRow
);
}
_renderItem = rowData => this.renderItem(rowData, this._rows);
_onRef = c => this.setRefs(c);
render() {
const { useSectionList, renderListView, ...props } = this.props;
if (renderListView) {
// Ideally renderRow should be deprecated. We do this check for
// legacy purposes to not break certain renderListView cases
const useRenderRow = !!this.props.renderRow;
return renderListView(
props,
this.setRefs.bind(this),
this.onScroll.bind(this),
useRenderRow
? this.renderRow.bind(this, this._rows)
: this.renderItem.bind(this)
);
}
if (useSectionList) {
const ListComponent = this.props.useAnimatedList
? Animated.SectionList
: SectionList;
return (
<ListComponent
{...props}
{...this.listViewProps}
ref={this._onRef}
onScroll={this._onScroll}
renderItem={this._renderItem}
/>
);
}
const ListComponent = this.props.useAnimatedList
? Animated.FlatList
: FlatList;
return (
<ListComponent
{...props}
{...this.listViewProps}
ref={this._onRef}
onScroll={this._onScroll}
renderItem={this._renderItem}
/>
);
}
}
SwipeListView.propTypes = {
/**
* To render a custom ListView component, if you don't want to use ReactNative one.
* Note: This will call `renderRow`, not `renderItem`
*/
renderListView: PropTypes.func,
/**
* How to render a row in a FlatList. Should return a valid React Element.
*/
renderItem: PropTypes.func,
/**
* How to render a hidden row in a FlatList (renders behind the row). Should return a valid React Element.
* This is required unless renderItem is passing a SwipeRow.
*/
renderHiddenItem: PropTypes.func,
/**
* TranslateX value for opening the row to the left (positive number)
*/
leftOpenValue: PropTypes.number,
/**
* TranslateX value for opening the row to the right (negative number)
*/
rightOpenValue: PropTypes.number,
/**
* TranslateX value for firing onLeftActionStatusChange (positive number)
*/
leftActivationValue: PropTypes.number,
/**
* TranslateX value for firing onRightActionStatusChange (negative number)
*/
rightActivationValue: PropTypes.number,
/**
* TranslateX value for left action to which the row will be shifted after gesture release
*/
leftActionValue: PropTypes.number,
/**
* TranslateX value for right action to which the row will be shifted after gesture release
*/
rightActionValue: PropTypes.number,
/**
* Initial value for left action state (default is false)
*/
initialLeftActionState: PropTypes.bool,
/**
* Initial value for right action state (default is false)
*/
initialRightActionState: PropTypes.bool,
/**
* TranslateX value for stop the row to the left (positive number)
*/
stopLeftSwipe: PropTypes.number,
/**
* TranslateX value for stop the row to the right (negative number)
*/
stopRightSwipe: PropTypes.number,
/**
* Should open rows be closed when the listView begins scrolling
*/
closeOnScroll: PropTypes.bool,
/**
* Should open rows be closed when a row is pressed
*/
closeOnRowPress: PropTypes.bool,
/**
* Should open rows be closed when a row begins to swipe open
*/
closeOnRowBeginSwipe: PropTypes.bool,
/**
* Should open rows be closed when another row is opened
*/
closeOnRowOpen: PropTypes.bool,
/**
* Disable ability to swipe rows left
*/
disableLeftSwipe: PropTypes.bool,
/**
* Disable ability to swipe rows right
*/
disableRightSwipe: PropTypes.bool,
/**
* Enable hidden row onLayout calculations to run always.
*
* By default, hidden row size calculations are only done on the first onLayout event
* for performance reasons.
* Passing ```true``` here will cause calculations to run on every onLayout event.
* You may want to do this if your rows' sizes can change.
* One case is a SwipeListView with rows of different heights and an options to delete rows.
*/
recalculateHiddenLayout: PropTypes.bool,
/**
* Disable hidden row onLayout calculations
*
* Instead, {width: '100%', height: '100%'} will be used.
* Improves performance by avoiding component updates, while still working with orientation changes.
*/
disableHiddenLayoutCalculation: PropTypes.bool,
/**
* Called when a swipe row is animating swipe
*/
swipeGestureBegan: PropTypes.func,
/**
* Called when user has ended their swipe gesture
*/
swipeGestureEnded: PropTypes.func,
/**
* Called when a swipe row is animating open
*/
onRowOpen: PropTypes.func,
/**
* Called when a swipe row has animated open
*/
onRowDidOpen: PropTypes.func,
/**
* Called when a swipe row is animating closed
*/
onRowClose: PropTypes.func,
/**
* Called when a swipe row has animated closed
*/
onRowDidClose: PropTypes.func,
/**
* Called when row shifted to leftActivationValue
*/
onLeftAction: PropTypes.func,
/**
* Called when row shifted to rightActivationValue
*/
onRightAction: PropTypes.func,
/**
* Called once when swipe value crosses the leftActivationValue
*/
onLeftActionStatusChange: PropTypes.func,
/**
* Called once when swipe value crosses the rightActivationValue
*/
onRightActionStatusChange: PropTypes.func,
/**
* Called when scrolling on the SwipeListView has been enabled/disabled
*/
onScrollEnabled: PropTypes.func,
/**
* Called when a scroll event is emitted
*/
onScroll: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
/**
* Styles for the parent wrapper View of the SwipeRow
*/
swipeRowStyle: PropTypes.object,
/**
* Called when the FlatList ref is set and passes a ref to the FlatList
* e.g. listViewRef={ ref => this._swipeListViewRef = ref }
*/
listViewRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
/**
* Should the row with this key do a slide out preview to show that the list is swipeable
*/
previewRowKey: PropTypes.string,
/**
* [DEPRECATED] Should the first SwipeRow do a slide out preview to show that the list is swipeable
*/
previewFirstRow: PropTypes.bool,
/**
* [DEPRECATED] Should the specified rowId do a slide out preview to show that the list is swipeable
* Note: This ID will be passed to this function to get the correct row index
* https://facebook.github.io/react-native/docs/listviewdatasource.html#getrowidforflatindex
*/
previewRowIndex: PropTypes.number,
/**
* Duration of the slide out preview animation (milliseconds)
*/
previewDuration: PropTypes.number,
/**
* Should the animation repeat until false is provided
*/
previewRepeat: PropTypes.bool,
/**
* Time between each full completed animation in milliseconds
* Default: 1000 (1 second)
*/
previewRepeatDelay: PropTypes.number,
/**
* Delay of the slide out preview animation (milliseconds) // default 700ms
*/
previewOpenDelay: PropTypes.number,
/**
* TranslateX value for the slide out preview animation
* Default: 0.5 * props.rightOpenValue
*/
previewOpenValue: PropTypes.number,
/**
* Friction for the open / close animation
*/
friction: PropTypes.number,
/**
* Tension for the open / close animation
*/
tension: PropTypes.number,
/**
* RestSpeedThreshold for the open / close animation
*/
restSpeedThreshold: PropTypes.number,
/**
* RestDisplacementThreshold for the open / close animation
*/
restDisplacementThreshold: PropTypes.number,
/**
* The dx value used to detect when a user has begun a swipe gesture
*/
directionalDistanceChangeThreshold: PropTypes.number,
/**
* What % of the left/right openValue does the user need to swipe
* past to trigger the row opening.
*/
swipeToOpenPercent: PropTypes.number,
/**
* Describes how much the ending velocity of the gesture affects whether the swipe will result in the item being closed or open.
* A velocity factor of 0 means that the velocity will have no bearing on whether the swipe settles on a closed or open position
* and it'll just take into consideration the swipeToOpenPercent.
*/
swipeToOpenVelocityContribution: PropTypes.number,
/**
* What % of the left/right openValue does the user need to swipe
* past to trigger the row closing.
*/
swipeToClosePercent: PropTypes.number,
/**
* callback to determine whether component should update (currentItem, newItem)
*/
shouldItemUpdate: PropTypes.func,
/**
* Callback invoked any time the swipe value of a SwipeRow is changed
*/
onSwipeValueChange: PropTypes.func,
/**
* useNativeDriver: true for all animations where possible
*/
useNativeDriver: PropTypes.bool,
/**
* Use Animated.Flatlist or Animated.Sectionlist
*/
useAnimatedList: PropTypes.bool,
/**
* keyExtractor: function to generate key value for each row in the list
*/
keyExtractor: PropTypes.func,
/**
* Callback that runs after row swipe preview is finished
*/
onPreviewEnd: PropTypes.func,
};
SwipeListView.defaultProps = {
leftOpenValue: 0,
rightOpenValue: 0,
closeOnRowBeginSwipe: false,
closeOnScroll: true,
closeOnRowPress: true,
closeOnRowOpen: true,
disableLeftSwipe: false,
disableRightSwipe: false,
recalculateHiddenLayout: false,
disableHiddenLayoutCalculation: false,
previewFirstRow: false,
directionalDistanceChangeThreshold: 2,
swipeToOpenPercent: 50,
swipeToOpenVelocityContribution: 0,
swipeToClosePercent: 50,
useNativeDriver: true,
previewRepeat: false,
previewRepeatDelay: 1000,
useAnimatedList: false,
};
export default SwipeListView;