@momo-kits/swipe 0.0.60-beta → 0.0.60-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SwipeAction.js CHANGED
@@ -10,20 +10,19 @@
10
10
  /* eslint-disable no-unused-expressions */
11
11
  /* eslint-disable react/destructuring-assignment */
12
12
 
13
- import React, {
14
- Component,
15
- } from 'react';
13
+ import React, { Component } from 'react';
16
14
  import PropTypes from 'prop-types';
17
15
  import {
18
- Dimensions,
19
- Animated,
20
- PanResponder,
21
- StyleSheet,
22
- TouchableOpacity,
23
- View,
24
- Image
16
+ Dimensions,
17
+ Animated,
18
+ PanResponder,
19
+ StyleSheet,
20
+ TouchableOpacity,
21
+ View,
22
+ Image,
25
23
  } from 'react-native';
26
24
  import { Colors, ValueUtil, Text } from '@momo-kits/core';
25
+ import { TouchableHighlight } from 'react-native';
27
26
 
28
27
  const DEFAULT_PREVIEW_OPEN_DELAY = 700;
29
28
  const PREVIEW_CLOSE_DELAY = 300;
@@ -32,625 +31,688 @@ const SCROLL_LOCK_MILLISECONDS = 300;
32
31
  const SCREEN_WIDTH = Dimensions.get('window').width;
33
32
  const WIDTH_HIDDEN_RIGHT = 190;
34
33
  class SwipeAction extends Component {
35
- constructor(props) {
36
- super(props);
37
- this.isOpen = false;
38
- this.previousTrackedTranslateX = 0;
39
- this.previousTrackedDirection = null;
40
- this.horizontalSwipeGestureBegan = false;
41
- this.swipeInitialX = null;
42
- this.parentScrollEnabled = true;
43
- this.ranPreview = false;
44
- this._ensureScrollEnabledTimer = null;
45
- this.isForceClosing = false;
46
- this.state = {
47
- dimensionsSet: false,
48
- hiddenHeight: this.props.disableHiddenLayoutCalculation ? '100%' : 0,
49
- hiddenWidth: this.props.disableHiddenLayoutCalculation ? '100%' : 0,
50
- isLeftActionVisible: false,
51
- isRightActionVisible: false,
52
- leftOpenValue: 0,
53
- rightOpenValue: 0
54
- };
55
- const { leftAction, rightAction } = this.props;
56
- const buttonCellWidth = WIDTH_HIDDEN_RIGHT / 2;
57
- // leftAction ? this.leftOpenValue = leftAction.length * buttonCellWidth : 0;
58
- // rightAction ? this.rightOpenValue = -rightAction.length * buttonCellWidth : 0;
59
- this.stopLeftSwipe = SCREEN_WIDTH;
60
- this.stopRightSwipe = -SCREEN_WIDTH;
61
- this._translateX = new Animated.Value(0);
62
- if (this.props.onSwipeValueChange) {
63
- this._translateX.addListener(({ value }) => {
64
- let direction = this.previousTrackedDirection;
65
- if (value !== this.previousTrackedTranslateX) {
66
- direction = value > this.previousTrackedTranslateX ? 'right' : 'left';
67
- }
68
- this.props.onSwipeValueChange && this.props.onSwipeValueChange({
69
- isOpen: this.isOpen,
70
- direction,
71
- value,
72
- });
73
- this.previousTrackedTranslateX = value;
74
- this.previousTrackedDirection = direction;
75
- });
76
- }
77
-
78
- if (this.props.forceCloseToRightThreshold && this.props.forceCloseToRightThreshold > 0) {
79
- this._translateX.addListener(({ value }) => {
80
- if (!this.isForceClosing && (SCREEN_WIDTH + value) < this.props.forceCloseToRightThreshold) {
81
- this.isForceClosing = true;
82
- this.forceCloseRow('right');
83
- if (this.props.onForceCloseToRight) {
84
- this.props.onForceCloseToRight();
85
- }
86
- }
87
- });
88
- }
89
-
90
- if (this.props.forceCloseToLeftThreshold && this.props.forceCloseToLeftThreshold > 0) {
91
- this._translateX.addListener(({ value }) => {
92
- if (!this.isForceClosing && (SCREEN_WIDTH - value) < this.props.forceCloseToLeftThreshold) {
93
- this.isForceClosing = true;
94
- this.forceCloseRow('left');
95
- if (this.props.onForceCloseToLeft) {
96
- this.props.onForceCloseToLeft();
97
- }
98
- }
99
- });
100
- }
101
- }
102
-
103
- UNSAFE_componentWillMount() {
104
- this._panResponder = PanResponder.create({
105
- onMoveShouldSetPanResponder: (e, gs) => this.handleOnMoveShouldSetPanResponder(e, gs),
106
- onPanResponderMove: (e, gs) => this.handlePanResponderMove(e, gs),
107
- onPanResponderRelease: (e, gs) => this.handlePanResponderEnd(e, gs),
108
- onPanResponderTerminate: (e, gs) => this.handlePanResponderEnd(e, gs),
109
- onShouldBlockNativeResponder: (_) => false,
110
- });
111
- }
112
-
113
- componentWillUnmount() {
114
- clearTimeout(this._ensureScrollEnabledTimer);
115
- this._translateX.removeAllListeners();
116
- }
117
-
118
- shouldComponentUpdate(nextProps, nextState) {
119
- if (this.state.hiddenHeight !== nextState.hiddenHeight
120
- || this.state.hiddenWidth !== nextState.hiddenWidth
121
- || !this.props.shouldItemUpdate
122
- || (this.props.shouldItemUpdate && this.props.shouldItemUpdate(this.props.item, nextProps.item))) {
123
- return true;
124
- }
125
-
126
- return false;
127
- }
128
-
129
- getPreviewAnimation(toValue, delay) {
130
- return Animated.timing(
131
- this._translateX,
132
- {
133
- duration: this.props.previewDuration, toValue, delay, useNativeDriver: this.props.useNativeDriver
134
- }
135
- );
136
- }
137
-
138
- onContentLayout(e) {
139
- this.setState({
140
- dimensionsSet: !this.props.recalculateHiddenLayout,
141
- ...(!this.props.disableHiddenLayoutCalculation ? {
142
- hiddenHeight: e.nativeEvent.layout.height,
143
- hiddenWidth: e.nativeEvent.layout.width,
144
- } : {})
145
- });
146
-
147
- if (this.props.preview && !this.ranPreview) {
148
- this.ranPreview = true;
149
- const previewOpenValue = this.props.previewOpenValue || this.state.rightOpenValue * 0.5;
150
- this.getPreviewAnimation(previewOpenValue, this.props.previewOpenDelay)
151
- .start((_) => {
152
- this.getPreviewAnimation(0, PREVIEW_CLOSE_DELAY).start();
153
- });
154
- }
155
- }
156
-
157
- onPress() {
158
- if (this.swipeInitialX == null || this.swipeInitialX === 0) {
159
- if (this.props.onPress && typeof this.props.onPress === 'function') {
160
- this.props.onPress();
161
- }
162
- } else {
163
- if (this.props.closeOnPress) {
164
- this.closeRow();
165
- }
166
- }
167
- }
168
-
169
- handleOnMoveShouldSetPanResponder(e, gs) {
170
- const { dx } = gs;
171
- return Math.abs(dx) > this.props.directionalDistanceChangeThreshold;
172
- }
173
-
174
- handlePanResponderMove(e, gestureState) {
175
- /* If the view is force closing, then ignore Moves. Return */
176
- if (this.isForceClosing) {
177
- return;
178
- }
179
-
180
- /* Else, do normal job */
181
- const { dx, dy } = gestureState;
182
- const absDx = Math.abs(dx);
183
- const absDy = Math.abs(dy);
184
-
185
- // this check may not be necessary because we don't capture the move until we pass the threshold
186
- // just being extra safe here
187
- if (absDx > this.props.directionalDistanceChangeThreshold || absDy > this.props.directionalDistanceChangeThreshold) {
188
- // we have enough to determine direction
189
- if (absDy > absDx && !this.horizontalSwipeGestureBegan) {
190
- // user is moving vertically, do nothing, listView will handle
191
- return;
192
- }
193
-
194
- // user is moving horizontally
195
- if (this.parentScrollEnabled) {
196
- // disable scrolling on the listView parent
197
- this.parentScrollEnabled = false;
198
- this.props.setScrollEnabled && this.props.setScrollEnabled(false);
199
- }
200
-
201
- if (this.swipeInitialX === null) {
202
- // set tranlateX value when user started swiping
203
- this.swipeInitialX = this._translateX._value;
204
- }
205
- if (!this.horizontalSwipeGestureBegan) {
206
- this.horizontalSwipeGestureBegan = true;
207
- const direction = dx > 0 ? 'TO_RIGHT' : 'TO_LEFT';
208
- this.props.swipeGestureBegan && this.props.swipeGestureBegan(direction);
209
- }
210
-
211
- let newDX = this.swipeInitialX + dx;
212
- if (this.props.disableRightAction && newDX < 0) { newDX = 0; }
213
- if (this.props.disableLeftAction && newDX > 0) { newDX = 0; }
214
-
215
- if (this.stopLeftSwipe && newDX > this.stopLeftSwipe) { newDX = this.stopLeftSwipe; }
216
- if (this.stopRightSwipe && newDX < this.stopRightSwipe) { newDX = this.stopRightSwipe; }
217
-
218
- // set action buttons visibility
219
- if (newDX > 0) {
220
- this.setState({
221
- isLeftActionVisible: true,
222
- isRightActionVisible: false
223
- });
224
- } else if (newDX < 0) {
225
- this.setState({
226
- isRightActionVisible: true,
227
- isLeftActionVisible: false,
228
- });
229
- }
230
-
231
- this._translateX.setValue(newDX);
232
- }
233
- }
234
-
235
- ensureScrollEnabled = () => {
236
- if (!this.parentScrollEnabled) {
237
- this.parentScrollEnabled = true;
238
- this.props.setScrollEnabled && this.props.setScrollEnabled(true);
239
- }
240
- }
241
-
242
- handlePanResponderEnd(e, gestureState) {
243
- /* PandEnd will reset the force-closing state when it's true. */
244
- if (this.isForceClosing) {
245
- this.isForceClosing = false;
246
- }
247
- // decide how much the velocity will affect the final position that the list item settles in.
248
- const { swipeToOpenVelocityContribution } = this.props;
249
- const possibleExtraPixels = this.state.rightOpenValue * (swipeToOpenVelocityContribution);
250
- const clampedVelocity = Math.min(gestureState.vx, MAX_VELOCITY_CONTRIBUTION);
251
- const projectedExtraPixels = possibleExtraPixels * (clampedVelocity / MAX_VELOCITY_CONTRIBUTION);
252
-
253
- // re-enable scrolling on listView parent
254
- this._ensureScrollEnabledTimer = setTimeout(this.ensureScrollEnabled, SCROLL_LOCK_MILLISECONDS);
255
-
256
- // finish up the animation
257
- let toValue = 0;
258
- if (this._translateX._value >= 0) {
259
- // trying to swipe right
260
- if (this.swipeInitialX < this._translateX._value) {
261
- if ((this._translateX._value - projectedExtraPixels) > this.state.leftOpenValue * (this.props.swipeToOpenPercent / 100)) {
262
- // we're more than halfway
263
- toValue = this.state.leftOpenValue;
264
- }
265
- } else if ((this._translateX._value - projectedExtraPixels) > this.state.leftOpenValue * (1 - (this.props.swipeToClosePercent / 100))) {
266
- toValue = this.state.leftOpenValue;
267
- }
268
- } else {
269
- // trying to swipe left
270
- if (this.swipeInitialX > this._translateX._value) {
271
- if ((this._translateX._value - projectedExtraPixels) < this.state.rightOpenValue * (this.props.swipeToOpenPercent / 100)) {
272
- // we're more than halfway
273
- toValue = this.state.rightOpenValue;
274
- }
275
- } else if ((this._translateX._value - projectedExtraPixels) < this.state.rightOpenValue * (1 - (this.props.swipeToClosePercent / 100))) {
276
- toValue = this.state.rightOpenValue;
277
- }
278
- }
279
-
280
- this.props.swipeGestureEnd && this.props.swipeGestureEnd();
281
- this.manuallySwipeRow(toValue);
282
- }
283
-
284
- /*
285
- * This method is called by SwipeListView
286
- */
287
- closeRow() {
288
- this.manuallySwipeRow(0);
289
- }
290
-
291
- /**
292
- * Force close the row toward the end of the given direction.
293
- * @param {String} direction The direction to force close.
294
- */
295
- forceCloseRow(direction) {
296
- this.manuallySwipeRow(0, () => {
297
- if (direction === 'right' && this.props.onForceCloseToRightEnd) {
298
- this.props.onForceCloseToRightEnd();
299
- } else if (direction === 'left' && this.props.onForceCloseToLeftEnd) {
300
- this.props.onForceCloseToLeftEnd();
301
- }
302
- });
303
- }
304
-
305
- closeRowWithoutAnimation() {
306
- this._translateX.setValue(0);
307
-
308
- this.ensureScrollEnabled();
309
- this.isOpen = false;
310
- this.props.onDidClose && this.props.onDidClose();
311
-
312
- this.props.onClose && this.props.onClose();
313
-
314
- this.swipeInitialX = null;
315
- this.horizontalSwipeGestureBegan = false;
316
- }
317
-
318
- manuallySwipeRow(toValue, onAnimationEnd) {
319
- Animated.spring(
320
- this._translateX,
321
- {
322
- toValue,
323
- friction: this.props.friction,
324
- tension: this.props.tension,
325
- useNativeDriver: this.props.useNativeDriver,
326
- }
327
- ).start((_) => {
328
- this._translateX.setValue(toValue);
329
- this.ensureScrollEnabled();
330
- if (toValue === 0) {
331
- this.isOpen = false;
332
- this.props.onDidClose && this.props.onDidClose();
333
- } else {
334
- this.isOpen = true;
335
- this.props.onDidOpen && this.props.onDidOpen(toValue);
336
- }
337
- if (onAnimationEnd) {
338
- onAnimationEnd();
339
- }
340
- });
341
-
342
- if (toValue === 0) {
343
- this.props.onClose && this.props.onClose();
344
- } else {
345
- this.props.onOpen && this.props.onOpen(toValue);
346
- }
347
-
348
- // reset everything
349
- this.swipeInitialX = toValue;
350
- this.horizontalSwipeGestureBegan = false;
351
- }
352
-
353
- renderVisibleContent() {
354
- if (this.props.children) {
355
- // handle touchables
356
- const { onPress } = this.props.children.props;
357
-
358
- if (onPress) {
359
- const newOnPress = (_) => {
360
- this.onPress();
361
- onPress();
362
- };
363
- return React.cloneElement(
364
- this.props.children,
365
- {
366
- ...this.props.children.props,
367
- onPress: newOnPress
368
- }
369
- );
370
- }
371
- return (
372
- <TouchableOpacity
373
- activeOpacity={1}
374
- onPress={(_) => this.onPress()}
375
- >
376
- {this.props.children}
377
- </TouchableOpacity>
378
- );
379
- }
380
- }
381
-
382
- renderRowContent() {
383
- // We do this annoying if statement for performance.
384
- // We don't want the onLayout func to run after it runs once.
385
- if (this.state.dimensionsSet) {
386
- return (
387
- <Animated.View
388
- manipulationModes={['translateX']}
389
- {...this._panResponder.panHandlers}
390
- style={{
391
- zIndex: 2,
392
- transform: [
393
- { translateX: this._translateX }
394
- ]
395
- }}
396
- >
397
- {this.renderVisibleContent()}
398
- </Animated.View>
399
- );
400
- }
401
- return (
402
- <Animated.View
403
- manipulationModes={['translateX']}
404
- {...this._panResponder.panHandlers}
405
- onLayout={(e) => this.onContentLayout(e)}
406
- style={{
407
- zIndex: 2,
408
- transform: [
409
- { translateX: this._translateX }
410
- ]
411
- }}
412
- >
413
- {this.renderVisibleContent()}
414
- </Animated.View>
415
- );
416
- }
417
-
418
- onLeftActionLayout = ({ nativeEvent }) => {
419
- this.setState({
420
- leftOpenValue: nativeEvent.layout.width
421
- });
422
- }
423
-
424
- onRightActionLayout = ({ nativeEvent }) => {
425
- this.setState({
426
- rightOpenValue: -nativeEvent.layout.width
427
- });
428
- }
429
-
430
- renderHiddenContent = () => {
431
- const {
432
- actionBackground, rowIndex, rightAction, leftAction
433
- } = this.props;
434
- const hiddenRowStyle = {
435
- ...styles.standaloneRowBack,
436
- ...{ backgroundColor: actionBackground }
437
- };
438
- return (
439
- <View style={hiddenRowStyle}>
440
- {
441
- this.state.isLeftActionVisible
442
- && (
443
- <View style={styles.leftItemRow}>
444
- <View style={{ flexDirection: 'row' }} onLayout={this.onLeftActionLayout}>
445
- {
446
- leftAction?.map?.((item, index) => this.renderLeftAction(item, index))
447
- }
448
- </View>
449
- </View>
450
- )
451
- }
452
- {
453
- this.state.isRightActionVisible
454
- && (
455
- <View style={styles.rightItemRow}>
456
- <View style={{ flexDirection: 'row' }} onLayout={this.onRightActionLayout}>
457
- {
458
- rightAction?.slice?.(0)?.reverse().map((item, index) => this.renderRightAction(item, index))
459
- }
460
- </View>
461
- </View>
462
- )
463
- }
464
-
465
- </View>
466
- );
467
- }
468
-
469
- renderIcon = (icon) => {
470
- const { actionIconStyle } = this.props;
471
- const iconSource = ValueUtil.getImageSource(icon);
472
- return (
473
- <Image
474
- source={iconSource}
475
- resizeMode="contain"
476
- style={[styles.icon, actionIconStyle]}
477
- />
478
- );
479
- }
480
-
481
- renderTitle = (title) => {
482
- const { actionTextStyle } = this.props;
483
- return (
484
- <Text.SubTitle style={[styles.text, actionTextStyle]}>
485
- {title}
486
- </Text.SubTitle>
487
- );
488
- }
489
-
490
- onActionPress = (item, rowIndex) => () => {
491
- const { onPress } = item;
492
- onPress?.(item, rowIndex);
493
- }
494
-
495
- renderRightAction = (item, index) => {
496
- const { rowIndex, renderRightAction } = this.props;
497
- if (renderRightAction) {
498
- return (
499
- <View key={item + index}>
500
- {renderRightAction({ item, index, rowIndex })}
501
- </View>
502
- );
503
- }
504
- return (
505
- <TouchableOpacity
506
- key={index.toString()}
507
- style={[
508
- styles.buttonContainer,
509
- { backgroundColor: item.color, height: this.state.hiddenHeight }
510
- ]}
511
- onPress={this.onActionPress(item, rowIndex)}
512
- >
513
- {item.icon && this.renderIcon(item.icon)}
514
- {item.title && this.renderTitle(item.title)}
515
- </TouchableOpacity>
516
- );
517
- }
518
-
519
- renderLeftAction = (item, index) => {
520
- const { rowIndex, renderLeftAction } = this.props;
521
- if (renderLeftAction) {
522
- return (
523
- <View key={item + index}>
524
- {renderLeftAction({ item, index, rowIndex })}
525
- </View>
526
- );
527
- }
528
- return (
529
- <TouchableOpacity
530
- key={index.toString()}
531
- style={[
532
- styles.buttonContainer,
533
- { backgroundColor: item.color, height: this.state.hiddenHeight }
534
- ]}
535
- onPress={this.onActionPress(item, rowIndex)}
536
- >
537
- {item.icon && this.renderIcon(item.icon)}
538
- {item.title && this.renderTitle(item.title)}
539
- </TouchableOpacity>
540
- );
541
- }
542
-
543
- render() {
544
- return (
545
- <View style={this.props.style ? this.props.style : styles.container}>
546
- <View style={[
547
- styles.hidden,
548
- {
549
- height: this.state.hiddenHeight,
550
- width: this.state.hiddenWidth,
551
- }
552
- ]}
553
- >
554
- {this.renderHiddenContent()}
555
- </View>
556
- {this.renderRowContent()}
557
- </View>
558
- );
559
- }
34
+ constructor(props) {
35
+ super(props);
36
+ this.isOpen = false;
37
+ this.previousTrackedTranslateX = 0;
38
+ this.previousTrackedDirection = null;
39
+ this.horizontalSwipeGestureBegan = false;
40
+ this.swipeInitialX = null;
41
+ this.parentScrollEnabled = true;
42
+ this.ranPreview = false;
43
+ this._ensureScrollEnabledTimer = null;
44
+ this.isForceClosing = false;
45
+ this.state = {
46
+ dimensionsSet: false,
47
+ hiddenHeight: this.props.disableHiddenLayoutCalculation
48
+ ? '100%'
49
+ : 0,
50
+ hiddenWidth: this.props.disableHiddenLayoutCalculation ? '100%' : 0,
51
+ isLeftActionVisible: false,
52
+ isRightActionVisible: false,
53
+ leftOpenValue: 0,
54
+ rightOpenValue: 0,
55
+ };
56
+ const { leftAction, rightAction } = this.props;
57
+ const buttonCellWidth = WIDTH_HIDDEN_RIGHT / 2;
58
+ // leftAction ? this.leftOpenValue = leftAction.length * buttonCellWidth : 0;
59
+ // rightAction ? this.rightOpenValue = -rightAction.length * buttonCellWidth : 0;
60
+ this.stopLeftSwipe = SCREEN_WIDTH;
61
+ this.stopRightSwipe = -SCREEN_WIDTH;
62
+ this._translateX = new Animated.Value(0);
63
+ if (this.props.onSwipeValueChange) {
64
+ this._translateX.addListener(({ value }) => {
65
+ let direction = this.previousTrackedDirection;
66
+ if (value !== this.previousTrackedTranslateX) {
67
+ direction =
68
+ value > this.previousTrackedTranslateX
69
+ ? 'right'
70
+ : 'left';
71
+ }
72
+ this.props.onSwipeValueChange &&
73
+ this.props.onSwipeValueChange({
74
+ isOpen: this.isOpen,
75
+ direction,
76
+ value,
77
+ });
78
+ this.previousTrackedTranslateX = value;
79
+ this.previousTrackedDirection = direction;
80
+ });
81
+ }
82
+
83
+ if (
84
+ this.props.forceCloseToRightThreshold &&
85
+ this.props.forceCloseToRightThreshold > 0
86
+ ) {
87
+ this._translateX.addListener(({ value }) => {
88
+ if (
89
+ !this.isForceClosing &&
90
+ SCREEN_WIDTH + value < this.props.forceCloseToRightThreshold
91
+ ) {
92
+ this.isForceClosing = true;
93
+ this.forceCloseRow('right');
94
+ if (this.props.onForceCloseToRight) {
95
+ this.props.onForceCloseToRight();
96
+ }
97
+ }
98
+ });
99
+ }
100
+
101
+ if (
102
+ this.props.forceCloseToLeftThreshold &&
103
+ this.props.forceCloseToLeftThreshold > 0
104
+ ) {
105
+ this._translateX.addListener(({ value }) => {
106
+ if (
107
+ !this.isForceClosing &&
108
+ SCREEN_WIDTH - value < this.props.forceCloseToLeftThreshold
109
+ ) {
110
+ this.isForceClosing = true;
111
+ this.forceCloseRow('left');
112
+ if (this.props.onForceCloseToLeft) {
113
+ this.props.onForceCloseToLeft();
114
+ }
115
+ }
116
+ });
117
+ }
118
+ }
119
+
120
+ UNSAFE_componentWillMount() {
121
+ this._panResponder = PanResponder.create({
122
+ onMoveShouldSetPanResponder: (e, gs) =>
123
+ this.handleOnMoveShouldSetPanResponder(e, gs),
124
+ onPanResponderMove: (e, gs) => this.handlePanResponderMove(e, gs),
125
+ onPanResponderRelease: (e, gs) => this.handlePanResponderEnd(e, gs),
126
+ onPanResponderTerminate: (e, gs) =>
127
+ this.handlePanResponderEnd(e, gs),
128
+ onShouldBlockNativeResponder: (_) => false,
129
+ });
130
+ }
131
+
132
+ componentWillUnmount() {
133
+ clearTimeout(this._ensureScrollEnabledTimer);
134
+ this._translateX.removeAllListeners();
135
+ }
136
+
137
+ shouldComponentUpdate(nextProps, nextState) {
138
+ if (
139
+ this.state.hiddenHeight !== nextState.hiddenHeight ||
140
+ this.state.hiddenWidth !== nextState.hiddenWidth ||
141
+ !this.props.shouldItemUpdate ||
142
+ (this.props.shouldItemUpdate &&
143
+ this.props.shouldItemUpdate(this.props.item, nextProps.item))
144
+ ) {
145
+ return true;
146
+ }
147
+
148
+ return false;
149
+ }
150
+
151
+ getPreviewAnimation(toValue, delay) {
152
+ return Animated.timing(this._translateX, {
153
+ duration: this.props.previewDuration,
154
+ toValue,
155
+ delay,
156
+ useNativeDriver: this.props.useNativeDriver,
157
+ });
158
+ }
159
+
160
+ onContentLayout(e) {
161
+ this.setState({
162
+ dimensionsSet: !this.props.recalculateHiddenLayout,
163
+ ...(!this.props.disableHiddenLayoutCalculation
164
+ ? {
165
+ hiddenHeight: e.nativeEvent.layout.height,
166
+ hiddenWidth: e.nativeEvent.layout.width,
167
+ }
168
+ : {}),
169
+ });
170
+
171
+ if (this.props.preview && !this.ranPreview) {
172
+ this.ranPreview = true;
173
+ const previewOpenValue =
174
+ this.props.previewOpenValue || this.state.rightOpenValue * 0.5;
175
+ this.getPreviewAnimation(
176
+ previewOpenValue,
177
+ this.props.previewOpenDelay,
178
+ ).start((_) => {
179
+ this.getPreviewAnimation(0, PREVIEW_CLOSE_DELAY).start();
180
+ });
181
+ }
182
+ }
183
+
184
+ onPress() {
185
+ if (this.swipeInitialX == null || this.swipeInitialX === 0) {
186
+ if (
187
+ this.props.onPress &&
188
+ typeof this.props.onPress === 'function'
189
+ ) {
190
+ this.props.onPress();
191
+ }
192
+ } else {
193
+ if (this.props.closeOnPress) {
194
+ this.closeRow();
195
+ }
196
+ }
197
+ }
198
+
199
+ handleOnMoveShouldSetPanResponder(e, gs) {
200
+ const { dx } = gs;
201
+ return Math.abs(dx) > this.props.directionalDistanceChangeThreshold;
202
+ }
203
+
204
+ handlePanResponderMove(e, gestureState) {
205
+ /* If the view is force closing, then ignore Moves. Return */
206
+ if (this.isForceClosing) {
207
+ return;
208
+ }
209
+
210
+ /* Else, do normal job */
211
+ const { dx, dy } = gestureState;
212
+ const absDx = Math.abs(dx);
213
+ const absDy = Math.abs(dy);
214
+
215
+ // this check may not be necessary because we don't capture the move until we pass the threshold
216
+ // just being extra safe here
217
+ if (
218
+ absDx > this.props.directionalDistanceChangeThreshold ||
219
+ absDy > this.props.directionalDistanceChangeThreshold
220
+ ) {
221
+ // we have enough to determine direction
222
+ if (absDy > absDx && !this.horizontalSwipeGestureBegan) {
223
+ // user is moving vertically, do nothing, listView will handle
224
+ return;
225
+ }
226
+
227
+ // user is moving horizontally
228
+ if (this.parentScrollEnabled) {
229
+ // disable scrolling on the listView parent
230
+ this.parentScrollEnabled = false;
231
+ this.props.setScrollEnabled &&
232
+ this.props.setScrollEnabled(false);
233
+ }
234
+
235
+ if (this.swipeInitialX === null) {
236
+ // set tranlateX value when user started swiping
237
+ this.swipeInitialX = this._translateX._value;
238
+ }
239
+ if (!this.horizontalSwipeGestureBegan) {
240
+ this.horizontalSwipeGestureBegan = true;
241
+ const direction = dx > 0 ? 'TO_RIGHT' : 'TO_LEFT';
242
+ this.props.swipeGestureBegan &&
243
+ this.props.swipeGestureBegan(direction);
244
+ }
245
+
246
+ let newDX = this.swipeInitialX + dx;
247
+ if (this.props.disableRightAction && newDX < 0) {
248
+ newDX = 0;
249
+ }
250
+ if (this.props.disableLeftAction && newDX > 0) {
251
+ newDX = 0;
252
+ }
253
+
254
+ if (this.stopLeftSwipe && newDX > this.stopLeftSwipe) {
255
+ newDX = this.stopLeftSwipe;
256
+ }
257
+ if (this.stopRightSwipe && newDX < this.stopRightSwipe) {
258
+ newDX = this.stopRightSwipe;
259
+ }
260
+
261
+ // set action buttons visibility
262
+ if (newDX > 0) {
263
+ this.setState({
264
+ isLeftActionVisible: true,
265
+ isRightActionVisible: false,
266
+ });
267
+ } else if (newDX < 0) {
268
+ this.setState({
269
+ isRightActionVisible: true,
270
+ isLeftActionVisible: false,
271
+ });
272
+ }
273
+
274
+ this._translateX.setValue(newDX);
275
+ }
276
+ }
277
+
278
+ ensureScrollEnabled = () => {
279
+ if (!this.parentScrollEnabled) {
280
+ this.parentScrollEnabled = true;
281
+ this.props.setScrollEnabled && this.props.setScrollEnabled(true);
282
+ }
283
+ };
284
+
285
+ handlePanResponderEnd(e, gestureState) {
286
+ /* PandEnd will reset the force-closing state when it's true. */
287
+ if (this.isForceClosing) {
288
+ this.isForceClosing = false;
289
+ }
290
+ // decide how much the velocity will affect the final position that the list item settles in.
291
+ const { swipeToOpenVelocityContribution } = this.props;
292
+ const possibleExtraPixels =
293
+ this.state.rightOpenValue * swipeToOpenVelocityContribution;
294
+ const clampedVelocity = Math.min(
295
+ gestureState.vx,
296
+ MAX_VELOCITY_CONTRIBUTION,
297
+ );
298
+ const projectedExtraPixels =
299
+ possibleExtraPixels * (clampedVelocity / MAX_VELOCITY_CONTRIBUTION);
300
+
301
+ // re-enable scrolling on listView parent
302
+ this._ensureScrollEnabledTimer = setTimeout(
303
+ this.ensureScrollEnabled,
304
+ SCROLL_LOCK_MILLISECONDS,
305
+ );
306
+
307
+ // finish up the animation
308
+ let toValue = 0;
309
+ if (this._translateX._value >= 0) {
310
+ // trying to swipe right
311
+ if (this.swipeInitialX < this._translateX._value) {
312
+ if (
313
+ this._translateX._value - projectedExtraPixels >
314
+ this.state.leftOpenValue *
315
+ (this.props.swipeToOpenPercent / 100)
316
+ ) {
317
+ // we're more than halfway
318
+ toValue = this.state.leftOpenValue;
319
+ }
320
+ } else if (
321
+ this._translateX._value - projectedExtraPixels >
322
+ this.state.leftOpenValue *
323
+ (1 - this.props.swipeToClosePercent / 100)
324
+ ) {
325
+ toValue = this.state.leftOpenValue;
326
+ }
327
+ } else {
328
+ // trying to swipe left
329
+ if (this.swipeInitialX > this._translateX._value) {
330
+ if (
331
+ this._translateX._value - projectedExtraPixels <
332
+ this.state.rightOpenValue *
333
+ (this.props.swipeToOpenPercent / 100)
334
+ ) {
335
+ // we're more than halfway
336
+ toValue = this.state.rightOpenValue;
337
+ }
338
+ } else if (
339
+ this._translateX._value - projectedExtraPixels <
340
+ this.state.rightOpenValue *
341
+ (1 - this.props.swipeToClosePercent / 100)
342
+ ) {
343
+ toValue = this.state.rightOpenValue;
344
+ }
345
+ }
346
+
347
+ this.props.swipeGestureEnd && this.props.swipeGestureEnd();
348
+ this.manuallySwipeRow(toValue);
349
+ }
350
+
351
+ /*
352
+ * This method is called by SwipeListView
353
+ */
354
+ closeRow() {
355
+ this.manuallySwipeRow(0);
356
+ }
357
+
358
+ /**
359
+ * Force close the row toward the end of the given direction.
360
+ * @param {String} direction The direction to force close.
361
+ */
362
+ forceCloseRow(direction) {
363
+ this.manuallySwipeRow(0, () => {
364
+ if (direction === 'right' && this.props.onForceCloseToRightEnd) {
365
+ this.props.onForceCloseToRightEnd();
366
+ } else if (
367
+ direction === 'left' &&
368
+ this.props.onForceCloseToLeftEnd
369
+ ) {
370
+ this.props.onForceCloseToLeftEnd();
371
+ }
372
+ });
373
+ }
374
+
375
+ closeRowWithoutAnimation() {
376
+ this._translateX.setValue(0);
377
+
378
+ this.ensureScrollEnabled();
379
+ this.isOpen = false;
380
+ this.props.onDidClose && this.props.onDidClose();
381
+
382
+ this.props.onClose && this.props.onClose();
383
+
384
+ this.swipeInitialX = null;
385
+ this.horizontalSwipeGestureBegan = false;
386
+ }
387
+
388
+ manuallySwipeRow(toValue, onAnimationEnd) {
389
+ Animated.spring(this._translateX, {
390
+ toValue,
391
+ friction: this.props.friction,
392
+ tension: this.props.tension,
393
+ useNativeDriver: this.props.useNativeDriver,
394
+ }).start((_) => {
395
+ this._translateX.setValue(toValue);
396
+ this.ensureScrollEnabled();
397
+ if (toValue === 0) {
398
+ this.isOpen = false;
399
+ this.props.onDidClose && this.props.onDidClose();
400
+ } else {
401
+ this.isOpen = true;
402
+ this.props.onDidOpen && this.props.onDidOpen(toValue);
403
+ }
404
+ if (onAnimationEnd) {
405
+ onAnimationEnd();
406
+ }
407
+ });
408
+
409
+ if (toValue === 0) {
410
+ this.props.onClose && this.props.onClose();
411
+ } else {
412
+ this.props.onOpen && this.props.onOpen(toValue);
413
+ }
414
+
415
+ // reset everything
416
+ this.swipeInitialX = toValue;
417
+ this.horizontalSwipeGestureBegan = false;
418
+ }
419
+
420
+ renderVisibleContent() {
421
+ if (this.props.children) {
422
+ // handle touchables
423
+ const { onPress } = this.props.children.props;
424
+
425
+ if (onPress) {
426
+ const newOnPress = (_) => {
427
+ this.onPress();
428
+ onPress();
429
+ };
430
+ return React.cloneElement(this.props.children, {
431
+ ...this.props.children.props,
432
+ onPress: newOnPress,
433
+ });
434
+ }
435
+ return (
436
+ <TouchableHighlight
437
+ activeOpacity={0.8}
438
+ underlayColor="white"
439
+ onPress={(_) => this.onPress()}>
440
+ {this.props.children}
441
+ </TouchableHighlight>
442
+ );
443
+ }
444
+ }
445
+
446
+ renderRowContent() {
447
+ // We do this annoying if statement for performance.
448
+ // We don't want the onLayout func to run after it runs once.
449
+ if (this.state.dimensionsSet) {
450
+ return (
451
+ <Animated.View
452
+ manipulationModes={['translateX']}
453
+ {...this._panResponder.panHandlers}
454
+ style={{
455
+ zIndex: 2,
456
+ transform: [{ translateX: this._translateX }],
457
+ }}>
458
+ {this.renderVisibleContent()}
459
+ </Animated.View>
460
+ );
461
+ }
462
+ return (
463
+ <Animated.View
464
+ manipulationModes={['translateX']}
465
+ {...this._panResponder.panHandlers}
466
+ onLayout={(e) => this.onContentLayout(e)}
467
+ style={{
468
+ zIndex: 2,
469
+ transform: [{ translateX: this._translateX }],
470
+ }}>
471
+ {this.renderVisibleContent()}
472
+ </Animated.View>
473
+ );
474
+ }
475
+
476
+ onLeftActionLayout = ({ nativeEvent }) => {
477
+ this.setState({
478
+ leftOpenValue: nativeEvent.layout.width,
479
+ });
480
+ };
481
+
482
+ onRightActionLayout = ({ nativeEvent }) => {
483
+ this.setState({
484
+ rightOpenValue: -nativeEvent.layout.width,
485
+ });
486
+ };
487
+
488
+ renderHiddenContent = () => {
489
+ const { actionBackground, rowIndex, rightAction, leftAction } =
490
+ this.props;
491
+ const hiddenRowStyle = {
492
+ ...styles.standaloneRowBack,
493
+ ...{ backgroundColor: actionBackground },
494
+ };
495
+ return (
496
+ <View style={hiddenRowStyle}>
497
+ {this.state.isLeftActionVisible && (
498
+ <View style={styles.leftItemRow}>
499
+ <View
500
+ style={{ flexDirection: 'row' }}
501
+ onLayout={this.onLeftActionLayout}>
502
+ {leftAction?.map?.((item, index) =>
503
+ this.renderLeftAction(item, index),
504
+ )}
505
+ </View>
506
+ </View>
507
+ )}
508
+ {this.state.isRightActionVisible && (
509
+ <View style={styles.rightItemRow}>
510
+ <View
511
+ style={{ flexDirection: 'row' }}
512
+ onLayout={this.onRightActionLayout}>
513
+ {rightAction
514
+ ?.slice?.(0)
515
+ ?.reverse()
516
+ .map((item, index) =>
517
+ this.renderRightAction(item, index),
518
+ )}
519
+ </View>
520
+ </View>
521
+ )}
522
+ </View>
523
+ );
524
+ };
525
+
526
+ renderIcon = (icon) => {
527
+ const { actionIconStyle } = this.props;
528
+ const iconSource = ValueUtil.getImageSource(icon);
529
+ return (
530
+ <Image
531
+ source={iconSource}
532
+ resizeMode="contain"
533
+ style={[styles.icon, actionIconStyle]}
534
+ />
535
+ );
536
+ };
537
+
538
+ renderTitle = (title) => {
539
+ const { actionTextStyle } = this.props;
540
+ return (
541
+ <Text.SubTitle style={[styles.text, actionTextStyle]}>
542
+ {title}
543
+ </Text.SubTitle>
544
+ );
545
+ };
546
+
547
+ onActionPress = (item, rowIndex) => () => {
548
+ const { onPress } = item;
549
+ onPress?.(item, rowIndex);
550
+ };
551
+
552
+ renderRightAction = (item, index) => {
553
+ const { rowIndex, renderRightAction } = this.props;
554
+ if (renderRightAction) {
555
+ return (
556
+ <View key={item + index}>
557
+ {renderRightAction({ item, index, rowIndex })}
558
+ </View>
559
+ );
560
+ }
561
+ return (
562
+ <TouchableOpacity
563
+ key={index.toString()}
564
+ style={[
565
+ styles.buttonContainer,
566
+ {
567
+ backgroundColor: item.color,
568
+ height: this.state.hiddenHeight,
569
+ },
570
+ ]}
571
+ onPress={this.onActionPress(item, rowIndex)}>
572
+ {item.icon && this.renderIcon(item.icon)}
573
+ {item.title && this.renderTitle(item.title)}
574
+ </TouchableOpacity>
575
+ );
576
+ };
577
+
578
+ renderLeftAction = (item, index) => {
579
+ const { rowIndex, renderLeftAction } = this.props;
580
+ if (renderLeftAction) {
581
+ return (
582
+ <View key={item + index}>
583
+ {renderLeftAction({ item, index, rowIndex })}
584
+ </View>
585
+ );
586
+ }
587
+ return (
588
+ <TouchableOpacity
589
+ key={index.toString()}
590
+ style={[
591
+ styles.buttonContainer,
592
+ {
593
+ backgroundColor: item.color,
594
+ height: this.state.hiddenHeight,
595
+ },
596
+ ]}
597
+ onPress={this.onActionPress(item, rowIndex)}>
598
+ {item.icon && this.renderIcon(item.icon)}
599
+ {item.title && this.renderTitle(item.title)}
600
+ </TouchableOpacity>
601
+ );
602
+ };
603
+
604
+ render() {
605
+ return (
606
+ <View
607
+ style={this.props.style ? this.props.style : styles.container}>
608
+ <View
609
+ style={[
610
+ styles.hidden,
611
+ {
612
+ height: this.state.hiddenHeight,
613
+ width: this.state.hiddenWidth,
614
+ },
615
+ ]}>
616
+ {this.renderHiddenContent()}
617
+ </View>
618
+ {this.renderRowContent()}
619
+ </View>
620
+ );
621
+ }
560
622
  }
561
623
 
562
624
  const styles = StyleSheet.create({
563
- container: {
564
- // As of RN 0.29 flex: 1 is causing all rows to be the same height
565
- // flex: 1
566
- },
567
- hidden: {
568
- zIndex: 1,
569
- bottom: 0,
570
- left: 0,
571
- overflow: 'hidden',
572
- position: 'absolute',
573
- right: 0,
574
- top: 0,
575
- },
576
- standaloneRowBack: {
577
- backgroundColor: Colors.white,
578
- alignItems: 'center',
579
- flex: 1,
580
- flexDirection: 'row',
581
- justifyContent: 'space-between'
582
- },
583
- leftItemRow: {
584
- alignItems: 'center',
585
- flex: 1,
586
- flexDirection: 'row',
587
- justifyContent: 'flex-start',
588
- paddingRight: 10
589
- },
590
- rightItemRow: {
591
- alignItems: 'center',
592
- flex: 1,
593
- flexDirection: 'row',
594
- justifyContent: 'flex-end',
595
- paddingLeft: 10,
596
- },
597
- buttonContainer: {
598
- backgroundColor: 'red',
599
- alignItems: 'center',
600
- justifyContent: 'center',
601
- width: 88
602
- },
603
- icon: {
604
- height: 24,
605
- width: 24,
606
- tintColor: 'white'
607
- },
608
- text: {
609
- color: Colors.white,
610
- textAlign: 'center',
611
- marginHorizontal: 10,
612
- marginTop: 10
613
- }
625
+ container: {
626
+ // As of RN 0.29 flex: 1 is causing all rows to be the same height
627
+ // flex: 1
628
+ },
629
+ hidden: {
630
+ zIndex: 1,
631
+ bottom: 0,
632
+ left: 0,
633
+ overflow: 'hidden',
634
+ position: 'absolute',
635
+ right: 0,
636
+ top: 0,
637
+ },
638
+ standaloneRowBack: {
639
+ backgroundColor: Colors.white,
640
+ alignItems: 'center',
641
+ flex: 1,
642
+ flexDirection: 'row',
643
+ justifyContent: 'space-between',
644
+ },
645
+ leftItemRow: {
646
+ alignItems: 'center',
647
+ flex: 1,
648
+ flexDirection: 'row',
649
+ justifyContent: 'flex-start',
650
+ paddingRight: 10,
651
+ },
652
+ rightItemRow: {
653
+ alignItems: 'center',
654
+ flex: 1,
655
+ flexDirection: 'row',
656
+ justifyContent: 'flex-end',
657
+ paddingLeft: 10,
658
+ },
659
+ buttonContainer: {
660
+ backgroundColor: 'red',
661
+ alignItems: 'center',
662
+ justifyContent: 'center',
663
+ width: 88,
664
+ },
665
+ icon: {
666
+ height: 24,
667
+ width: 24,
668
+ tintColor: 'white',
669
+ },
670
+ text: {
671
+ color: Colors.white,
672
+ textAlign: 'center',
673
+ marginHorizontal: 10,
674
+ marginTop: 10,
675
+ },
614
676
  });
615
677
 
616
678
  SwipeAction.propTypes = {
617
- onOpen: PropTypes.func,
618
- closeOnPress: PropTypes.bool,
619
- disableRightAction: PropTypes.bool,
620
- disableLeftAction: PropTypes.bool,
621
- onClose: PropTypes.func,
622
- style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
623
- leftAction: PropTypes.array,
624
- rightAction: PropTypes.array,
625
- renderRightAction: PropTypes.func,
626
- renderLeftAction: PropTypes.func,
627
- onPress: PropTypes.func,
628
- children: PropTypes.element.isRequired,
629
- actionBackground: PropTypes.string,
630
- actionTextStyle: PropTypes.object,
631
- swipeGestureBegan: PropTypes.func,
632
- swipeGestureEnd: PropTypes.func,
679
+ onOpen: PropTypes.func,
680
+ closeOnPress: PropTypes.bool,
681
+ disableRightAction: PropTypes.bool,
682
+ disableLeftAction: PropTypes.bool,
683
+ onClose: PropTypes.func,
684
+ style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
685
+ leftAction: PropTypes.array,
686
+ rightAction: PropTypes.array,
687
+ renderRightAction: PropTypes.func,
688
+ renderLeftAction: PropTypes.func,
689
+ onPress: PropTypes.func,
690
+ children: PropTypes.element.isRequired,
691
+ actionBackground: PropTypes.string,
692
+ actionTextStyle: PropTypes.object,
693
+ swipeGestureBegan: PropTypes.func,
694
+ swipeGestureEnd: PropTypes.func,
633
695
  };
634
696
 
635
697
  SwipeAction.defaultProps = {
636
- closeOnPress: true,
637
- disableRightAction: true,
638
- disableLeftAction: true,
639
- recalculateHiddenLayout: false,
640
- disableHiddenLayoutCalculation: false,
641
- preview: false,
642
- previewDuration: 300,
643
- previewOpenDelay: DEFAULT_PREVIEW_OPEN_DELAY,
644
- directionalDistanceChangeThreshold: 2,
645
- swipeToOpenPercent: 50,
646
- swipeToOpenVelocityContribution: 0,
647
- swipeToClosePercent: 50,
648
- swipeToPerformActionPercent: 50,
649
- item: {},
650
- useNativeDriver: true,
651
- rightAction: [],
652
- leftAction: [],
653
- actionBackground: Colors.white
698
+ closeOnPress: true,
699
+ disableRightAction: true,
700
+ disableLeftAction: true,
701
+ recalculateHiddenLayout: false,
702
+ disableHiddenLayoutCalculation: false,
703
+ preview: false,
704
+ previewDuration: 300,
705
+ previewOpenDelay: DEFAULT_PREVIEW_OPEN_DELAY,
706
+ directionalDistanceChangeThreshold: 2,
707
+ swipeToOpenPercent: 50,
708
+ swipeToOpenVelocityContribution: 0,
709
+ swipeToClosePercent: 50,
710
+ swipeToPerformActionPercent: 50,
711
+ item: {},
712
+ useNativeDriver: true,
713
+ rightAction: [],
714
+ leftAction: [],
715
+ actionBackground: Colors.white,
654
716
  };
655
717
 
656
718
  export default SwipeAction;