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