react-native-advanced-text 0.2.16 → 0.2.17
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/README.md +9 -0
- package/android/src/main/java/com/advancedtext/AdvancedTextView.kt +144 -4
- package/android/src/main/java/com/advancedtext/AdvancedTextViewManager.kt +7 -0
- package/ios/AdvancedTextView.mm +140 -0
- package/lib/module/AdvancedText.js +7 -0
- package/lib/module/AdvancedText.js.map +1 -1
- package/lib/module/AdvancedTextViewNativeComponent.ts +15 -1
- package/lib/typescript/src/AdvancedText.d.ts.map +1 -1
- package/lib/typescript/src/AdvancedTextViewNativeComponent.d.ts +11 -0
- package/lib/typescript/src/AdvancedTextViewNativeComponent.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/AdvancedText.tsx +6 -0
- package/src/AdvancedTextViewNativeComponent.ts +15 -1
package/README.md
CHANGED
|
@@ -52,6 +52,15 @@ import { AdvancedText } from "react-native-advanced-text";
|
|
|
52
52
|
/>
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
## Word gestures
|
|
56
|
+
|
|
57
|
+
| Prop | Fires on | Notes |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| `onWordPress` | a tap on a word | `{ word, index }` |
|
|
60
|
+
| `onWordDoublePress` | a double tap on a word | `{ word, index }`. A double tap never selects text. While this is set, a single tap waits out the double-tap delay (~300 ms) before `onWordPress`, so a double tap doesn't also fire it. |
|
|
61
|
+
| `onWordLongPress` | a long press on a word | `{ word, index }`. Text selection still starts as usual. |
|
|
62
|
+
| `onSelection` | the selection menu | `{ selectedText, event, startWordIndex, endWordIndex }`: the menu option, and the first and last word index the selection touches (`-1` if none). |
|
|
63
|
+
|
|
55
64
|
## Underlined words
|
|
56
65
|
|
|
57
66
|
An entry in `highlightedWords` with `underline: true` draws a line under the
|
|
@@ -24,6 +24,7 @@ import android.view.Menu
|
|
|
24
24
|
import android.view.MenuItem
|
|
25
25
|
import android.view.View
|
|
26
26
|
import android.view.MotionEvent
|
|
27
|
+
import android.view.ViewConfiguration
|
|
27
28
|
import android.view.accessibility.AccessibilityNodeInfo
|
|
28
29
|
import android.widget.TextView
|
|
29
30
|
import com.facebook.react.bridge.Arguments
|
|
@@ -40,6 +41,8 @@ class AdvancedTextView : TextView {
|
|
|
40
41
|
private var highlightedWords: List<HighlightedWord> = emptyList()
|
|
41
42
|
private var menuOptions: List<String> = emptyList()
|
|
42
43
|
private var lastSelectedText: String = ""
|
|
44
|
+
// First and last word index the selection touches, -1 for none.
|
|
45
|
+
private var lastSelectedWords: Pair<Int, Int> = -1 to -1
|
|
43
46
|
private var customActionMode: ActionMode? = null
|
|
44
47
|
private var currentText: String = ""
|
|
45
48
|
private var textColor: String = "#000000"
|
|
@@ -68,6 +71,23 @@ class AdvancedTextView : TextView {
|
|
|
68
71
|
private val rangeBounds = RectF()
|
|
69
72
|
private val underlineBounds = RectF()
|
|
70
73
|
|
|
74
|
+
// Double taps are only told apart while the app handles them
|
|
75
|
+
// (onWordDoublePress); otherwise a tap fires onWordPress at once.
|
|
76
|
+
private var wordDoublePressEnabled = false
|
|
77
|
+
private val doubleTapTimeout = ViewConfiguration.getDoubleTapTimeout().toLong()
|
|
78
|
+
private var doubleTapSlopSquare = 0
|
|
79
|
+
private var lastTapUpTime = 0L
|
|
80
|
+
private var lastTapUpX = 0f
|
|
81
|
+
private var lastTapUpY = 0f
|
|
82
|
+
// From a double tap's second touch down to its up. The TextView never sees
|
|
83
|
+
// those events, so it can't select the word as it would on a double tap.
|
|
84
|
+
private var handlingDoubleTap = false
|
|
85
|
+
// A tap's onWordPress, held back while a second tap could still come.
|
|
86
|
+
private var pendingWordPress: Runnable? = null
|
|
87
|
+
private var lastDownX = 0f
|
|
88
|
+
private var lastDownY = 0f
|
|
89
|
+
private var longPressed = false
|
|
90
|
+
|
|
71
91
|
constructor(context: Context?) : super(context) { init() }
|
|
72
92
|
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { init() }
|
|
73
93
|
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init() }
|
|
@@ -93,6 +113,19 @@ class AdvancedTextView : TextView {
|
|
|
93
113
|
|
|
94
114
|
movementMethod = SmartMovementMethod
|
|
95
115
|
|
|
116
|
+
val slop = ViewConfiguration.get(context).scaledDoubleTapSlop
|
|
117
|
+
doubleTapSlopSquare = slop * slop
|
|
118
|
+
|
|
119
|
+
// Reports the word, then returns false so the TextView still starts a
|
|
120
|
+
// selection as usual.
|
|
121
|
+
setOnLongClickListener {
|
|
122
|
+
longPressed = true
|
|
123
|
+
wordAt(lastDownX, lastDownY)?.let {
|
|
124
|
+
sendWordEvent("onWordLongPress", it.word, it.index)
|
|
125
|
+
}
|
|
126
|
+
false
|
|
127
|
+
}
|
|
128
|
+
|
|
96
129
|
customSelectionActionModeCallback = object : ActionMode.Callback {
|
|
97
130
|
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
|
98
131
|
customActionMode = mode
|
|
@@ -107,6 +140,7 @@ class AdvancedTextView : TextView {
|
|
|
107
140
|
|
|
108
141
|
if (selectionStart >= 0 && selectionEnd >= 0 && selectionStart != selectionEnd) {
|
|
109
142
|
lastSelectedText = text.subSequence(selectionStart, selectionEnd).toString()
|
|
143
|
+
lastSelectedWords = wordIndexRange(selectionStart, selectionEnd)
|
|
110
144
|
|
|
111
145
|
menuOptions.forEachIndexed { index, option ->
|
|
112
146
|
menu?.add(0, index, index, option)
|
|
@@ -525,6 +559,8 @@ class AdvancedTextView : TextView {
|
|
|
525
559
|
val event = Arguments.createMap().apply {
|
|
526
560
|
putString("selectedText", selectedText)
|
|
527
561
|
putString("event", eventType)
|
|
562
|
+
putInt("startWordIndex", lastSelectedWords.first)
|
|
563
|
+
putInt("endWordIndex", lastSelectedWords.second)
|
|
528
564
|
}
|
|
529
565
|
|
|
530
566
|
reactContext.getJSModule(RCTEventEmitter::class.java)
|
|
@@ -541,7 +577,18 @@ class AdvancedTextView : TextView {
|
|
|
541
577
|
|
|
542
578
|
override fun onClick(widget: View) {
|
|
543
579
|
Log.d(TAG, "Word clicked: '$word' (index=$wordIndex)")
|
|
544
|
-
|
|
580
|
+
if (!wordDoublePressEnabled) {
|
|
581
|
+
sendWordEvent("onWordPress", word, wordIndex)
|
|
582
|
+
return
|
|
583
|
+
}
|
|
584
|
+
// Wait to see whether this is the first tap of a double tap.
|
|
585
|
+
cancelPendingWordPress()
|
|
586
|
+
val press = Runnable {
|
|
587
|
+
pendingWordPress = null
|
|
588
|
+
sendWordEvent("onWordPress", word, wordIndex)
|
|
589
|
+
}
|
|
590
|
+
pendingWordPress = press
|
|
591
|
+
postDelayed(press, doubleTapTimeout)
|
|
545
592
|
}
|
|
546
593
|
|
|
547
594
|
// Skips super, which would paint the word in the link color and
|
|
@@ -551,7 +598,8 @@ class AdvancedTextView : TextView {
|
|
|
551
598
|
}
|
|
552
599
|
}
|
|
553
600
|
|
|
554
|
-
|
|
601
|
+
// onWordPress, onWordDoublePress or onWordLongPress.
|
|
602
|
+
private fun sendWordEvent(eventName: String, word: String, index: Int) {
|
|
555
603
|
try {
|
|
556
604
|
val reactContext = context as? ReactContext ?: return
|
|
557
605
|
val event = Arguments.createMap().apply {
|
|
@@ -560,10 +608,102 @@ class AdvancedTextView : TextView {
|
|
|
560
608
|
}
|
|
561
609
|
|
|
562
610
|
reactContext.getJSModule(RCTEventEmitter::class.java)
|
|
563
|
-
.receiveEvent(id,
|
|
611
|
+
.receiveEvent(id, eventName, event)
|
|
564
612
|
} catch (e: Exception) {
|
|
565
|
-
Log.e(TAG, "Error sending
|
|
613
|
+
Log.e(TAG, "Error sending $eventName event", e)
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
fun setWordDoublePressEnabled(enabled: Boolean) {
|
|
618
|
+
if (wordDoublePressEnabled == enabled) return
|
|
619
|
+
wordDoublePressEnabled = enabled
|
|
620
|
+
lastTapUpTime = 0L
|
|
621
|
+
// A tap held back for a double tap fires now instead of being lost.
|
|
622
|
+
pendingWordPress?.let {
|
|
623
|
+
removeCallbacks(it)
|
|
624
|
+
it.run()
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
629
|
+
when (event.actionMasked) {
|
|
630
|
+
MotionEvent.ACTION_DOWN -> {
|
|
631
|
+
lastDownX = event.x
|
|
632
|
+
lastDownY = event.y
|
|
633
|
+
longPressed = false
|
|
634
|
+
if (wordDoublePressEnabled && isSecondTap(event)) {
|
|
635
|
+
cancelPendingWordPress()
|
|
636
|
+
handlingDoubleTap = true
|
|
637
|
+
return true
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
MotionEvent.ACTION_UP -> if (handlingDoubleTap) {
|
|
641
|
+
handlingDoubleTap = false
|
|
642
|
+
lastTapUpTime = 0L
|
|
643
|
+
wordAt(event.x, event.y)?.let {
|
|
644
|
+
sendWordEvent("onWordDoublePress", it.word, it.index)
|
|
645
|
+
}
|
|
646
|
+
return true
|
|
647
|
+
}
|
|
648
|
+
MotionEvent.ACTION_CANCEL -> if (handlingDoubleTap) {
|
|
649
|
+
handlingDoubleTap = false
|
|
650
|
+
lastTapUpTime = 0L
|
|
651
|
+
return true
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (handlingDoubleTap) return true
|
|
655
|
+
|
|
656
|
+
val handled = super.onTouchEvent(event)
|
|
657
|
+
if (event.actionMasked == MotionEvent.ACTION_UP) {
|
|
658
|
+
// A long press isn't the first tap of a double tap.
|
|
659
|
+
if (longPressed) {
|
|
660
|
+
lastTapUpTime = 0L
|
|
661
|
+
} else {
|
|
662
|
+
lastTapUpTime = event.eventTime
|
|
663
|
+
lastTapUpX = event.x
|
|
664
|
+
lastTapUpY = event.y
|
|
665
|
+
}
|
|
566
666
|
}
|
|
667
|
+
return handled
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
override fun onDetachedFromWindow() {
|
|
671
|
+
cancelPendingWordPress()
|
|
672
|
+
super.onDetachedFromWindow()
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
private fun isSecondTap(down: MotionEvent): Boolean {
|
|
676
|
+
if (lastTapUpTime == 0L || down.eventTime - lastTapUpTime > doubleTapTimeout) {
|
|
677
|
+
return false
|
|
678
|
+
}
|
|
679
|
+
val dx = down.x - lastTapUpX
|
|
680
|
+
val dy = down.y - lastTapUpY
|
|
681
|
+
return dx * dx + dy * dy < doubleTapSlopSquare
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
private fun cancelPendingWordPress() {
|
|
685
|
+
pendingWordPress?.let { removeCallbacks(it) }
|
|
686
|
+
pendingWordPress = null
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// The word under a point in view coordinates, or null for blank space,
|
|
690
|
+
// including the space past the end of a line.
|
|
691
|
+
private fun wordAt(x: Float, y: Float): WordPosition? {
|
|
692
|
+
val currentLayout = layout ?: return null
|
|
693
|
+
val line = currentLayout.getLineForVertical((y - totalPaddingTop + scrollY).toInt())
|
|
694
|
+
val lineX = x - totalPaddingLeft + scrollX
|
|
695
|
+
if (lineX < currentLayout.getLineLeft(line) || lineX > currentLayout.getLineRight(line)) {
|
|
696
|
+
return null
|
|
697
|
+
}
|
|
698
|
+
val offset = getOffsetForPosition(x, y)
|
|
699
|
+
return wordPositions.find { offset >= it.start && offset <= it.end }
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// First and last word text[start, end) touches, or -1 to -1 for none.
|
|
703
|
+
private fun wordIndexRange(start: Int, end: Int): Pair<Int, Int> {
|
|
704
|
+
val touched = wordPositions.filter { it.start < end && it.end > start }
|
|
705
|
+
if (touched.isEmpty()) return -1 to -1
|
|
706
|
+
return touched.first().index to touched.last().index
|
|
567
707
|
}
|
|
568
708
|
|
|
569
709
|
fun clearSelection() {
|
|
@@ -161,9 +161,16 @@ class AdvancedTextViewManager : SimpleViewManager<AdvancedTextView>() {
|
|
|
161
161
|
view?.setAdvancedLetterSpacing(letterSpacing)
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
@ReactProp(name = "wordDoublePressEnabled")
|
|
165
|
+
fun setWordDoublePressEnabled(view: AdvancedTextView?, enabled: Boolean) {
|
|
166
|
+
view?.setWordDoublePressEnabled(enabled)
|
|
167
|
+
}
|
|
168
|
+
|
|
164
169
|
override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any> {
|
|
165
170
|
return mapOf(
|
|
166
171
|
"onWordPress" to mapOf("registrationName" to "onWordPress"),
|
|
172
|
+
"onWordDoublePress" to mapOf("registrationName" to "onWordDoublePress"),
|
|
173
|
+
"onWordLongPress" to mapOf("registrationName" to "onWordLongPress"),
|
|
167
174
|
"onSelection" to mapOf("registrationName" to "onSelection")
|
|
168
175
|
)
|
|
169
176
|
}
|
package/ios/AdvancedTextView.mm
CHANGED
|
@@ -91,6 +91,11 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
|
|
|
91
91
|
@interface AdvancedTextView () <RCTAdvancedTextViewViewProtocol, UIGestureRecognizerDelegate, UITextViewDelegate>
|
|
92
92
|
|
|
93
93
|
@property (nonatomic, strong) CustomTextView *textView;
|
|
94
|
+
@property (nonatomic, strong) UITapGestureRecognizer *singleTapGesture;
|
|
95
|
+
@property (nonatomic, strong) UITapGestureRecognizer *doubleTapGesture;
|
|
96
|
+
@property (nonatomic, strong) UILongPressGestureRecognizer *longPressGesture;
|
|
97
|
+
// On while the app handles double taps (onWordDoublePress).
|
|
98
|
+
@property (nonatomic, assign) BOOL wordDoublePressEnabled;
|
|
94
99
|
|
|
95
100
|
@end
|
|
96
101
|
|
|
@@ -177,6 +182,24 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
|
|
|
177
182
|
action:@selector(handleTap:)];
|
|
178
183
|
tapGesture.delegate = self;
|
|
179
184
|
[_textView addGestureRecognizer:tapGesture];
|
|
185
|
+
_singleTapGesture = tapGesture;
|
|
186
|
+
|
|
187
|
+
// Off until the app handles double taps, so taps aren't delayed.
|
|
188
|
+
UITapGestureRecognizer *doubleTapGesture = [[UITapGestureRecognizer alloc]
|
|
189
|
+
initWithTarget:self
|
|
190
|
+
action:@selector(handleDoubleTap:)];
|
|
191
|
+
doubleTapGesture.numberOfTapsRequired = 2;
|
|
192
|
+
doubleTapGesture.delegate = self;
|
|
193
|
+
doubleTapGesture.enabled = NO;
|
|
194
|
+
[_textView addGestureRecognizer:doubleTapGesture];
|
|
195
|
+
_doubleTapGesture = doubleTapGesture;
|
|
196
|
+
|
|
197
|
+
UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc]
|
|
198
|
+
initWithTarget:self
|
|
199
|
+
action:@selector(handleLongPress:)];
|
|
200
|
+
longPressGesture.delegate = self;
|
|
201
|
+
[_textView addGestureRecognizer:longPressGesture];
|
|
202
|
+
_longPressGesture = longPressGesture;
|
|
180
203
|
|
|
181
204
|
NSLog(@"[AdvancedTextView] Gesture recognizers setup successful");
|
|
182
205
|
} @catch (NSException *exception) {
|
|
@@ -228,6 +251,11 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
|
|
|
228
251
|
styleChanged = YES;
|
|
229
252
|
}
|
|
230
253
|
|
|
254
|
+
if (oldViewProps.wordDoublePressEnabled != newViewProps.wordDoublePressEnabled) {
|
|
255
|
+
_wordDoublePressEnabled = newViewProps.wordDoublePressEnabled;
|
|
256
|
+
_doubleTapGesture.enabled = _wordDoublePressEnabled;
|
|
257
|
+
}
|
|
258
|
+
|
|
231
259
|
if (oldViewProps.text != newViewProps.text) {
|
|
232
260
|
textChanged = YES;
|
|
233
261
|
NSLog(@"[AdvancedTextView] Text changed");
|
|
@@ -693,6 +721,89 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
|
|
|
693
721
|
}
|
|
694
722
|
}
|
|
695
723
|
|
|
724
|
+
- (void)handleDoubleTap:(UITapGestureRecognizer *)gesture
|
|
725
|
+
{
|
|
726
|
+
@try {
|
|
727
|
+
if (gesture.state != UIGestureRecognizerStateEnded) return;
|
|
728
|
+
|
|
729
|
+
// In case the text view selected anything before giving way.
|
|
730
|
+
_textView.selectedTextRange = nil;
|
|
731
|
+
|
|
732
|
+
NSInteger wordIndex = [self wordIndexAtPoint:[gesture locationInView:_textView]];
|
|
733
|
+
if (wordIndex >= 0 && wordIndex < _wordRanges.count) {
|
|
734
|
+
[self emitWordEvent:@"doublePress" word:_wordRanges[wordIndex][@"word"] index:wordIndex];
|
|
735
|
+
}
|
|
736
|
+
} @catch (NSException *exception) {
|
|
737
|
+
NSLog(@"[AdvancedTextView] Exception in handleDoubleTap: %@", exception);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Only reports the word: the text view's own long press still starts a selection.
|
|
742
|
+
- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture
|
|
743
|
+
{
|
|
744
|
+
@try {
|
|
745
|
+
if (gesture.state != UIGestureRecognizerStateBegan) return;
|
|
746
|
+
|
|
747
|
+
NSInteger wordIndex = [self wordIndexAtPoint:[gesture locationInView:_textView]];
|
|
748
|
+
if (wordIndex >= 0 && wordIndex < _wordRanges.count) {
|
|
749
|
+
[self emitWordEvent:@"longPress" word:_wordRanges[wordIndex][@"word"] index:wordIndex];
|
|
750
|
+
}
|
|
751
|
+
} @catch (NSException *exception) {
|
|
752
|
+
NSLog(@"[AdvancedTextView] Exception in handleLongPress: %@", exception);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
#pragma mark - UIGestureRecognizerDelegate
|
|
757
|
+
|
|
758
|
+
// While double taps are handled, a single tap waits to see whether a second
|
|
759
|
+
// one follows, so a double tap doesn't also fire onWordPress.
|
|
760
|
+
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
|
|
761
|
+
shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
|
|
762
|
+
{
|
|
763
|
+
return _wordDoublePressEnabled &&
|
|
764
|
+
gestureRecognizer == _singleTapGesture &&
|
|
765
|
+
otherGestureRecognizer == _doubleTapGesture;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// The text view's own double tap selects the word. It waits for ours, which
|
|
769
|
+
// wins on every double tap, so a double tap never selects text.
|
|
770
|
+
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
|
|
771
|
+
shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
|
|
772
|
+
{
|
|
773
|
+
if (!_wordDoublePressEnabled || gestureRecognizer != _doubleTapGesture) {
|
|
774
|
+
return NO;
|
|
775
|
+
}
|
|
776
|
+
if (![otherGestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
|
|
777
|
+
return NO;
|
|
778
|
+
}
|
|
779
|
+
return ((UITapGestureRecognizer *)otherGestureRecognizer).numberOfTapsRequired >= 2;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// The long press runs alongside the text view's own, which starts the selection.
|
|
783
|
+
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
|
|
784
|
+
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
|
|
785
|
+
{
|
|
786
|
+
return gestureRecognizer == _longPressGesture || otherGestureRecognizer == _longPressGesture;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// The first and last word a character range touches, or -1 for none.
|
|
790
|
+
- (void)wordIndexesForRange:(NSRange)range first:(NSInteger *)first last:(NSInteger *)last
|
|
791
|
+
{
|
|
792
|
+
*first = -1;
|
|
793
|
+
*last = -1;
|
|
794
|
+
for (NSDictionary *wordInfo in _wordRanges) {
|
|
795
|
+
NSRange wordRange = [wordInfo[@"range"] rangeValue];
|
|
796
|
+
if (NSIntersectionRange(wordRange, range).length == 0) {
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
NSInteger index = [wordInfo[@"index"] integerValue];
|
|
800
|
+
if (*first < 0) {
|
|
801
|
+
*first = index;
|
|
802
|
+
}
|
|
803
|
+
*last = index;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
696
807
|
#pragma mark - UITextViewDelegate
|
|
697
808
|
|
|
698
809
|
- (void)textViewDidChangeSelection:(UITextView *)textView
|
|
@@ -815,6 +926,29 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
|
|
|
815
926
|
}
|
|
816
927
|
}
|
|
817
928
|
|
|
929
|
+
// onWordDoublePress ("doublePress") or onWordLongPress ("longPress").
|
|
930
|
+
- (void)emitWordEvent:(NSString *)type word:(NSString *)word index:(NSInteger)index
|
|
931
|
+
{
|
|
932
|
+
@try {
|
|
933
|
+
if (!_eventEmitter) return;
|
|
934
|
+
auto emitter = std::static_pointer_cast<const AdvancedTextViewEventEmitter>(_eventEmitter);
|
|
935
|
+
|
|
936
|
+
if ([type isEqualToString:@"doublePress"]) {
|
|
937
|
+
AdvancedTextViewEventEmitter::OnWordDoublePress event;
|
|
938
|
+
event.word = [word UTF8String];
|
|
939
|
+
event.index = static_cast<int>(index);
|
|
940
|
+
emitter->onWordDoublePress(event);
|
|
941
|
+
} else {
|
|
942
|
+
AdvancedTextViewEventEmitter::OnWordLongPress event;
|
|
943
|
+
event.word = [word UTF8String];
|
|
944
|
+
event.index = static_cast<int>(index);
|
|
945
|
+
emitter->onWordLongPress(event);
|
|
946
|
+
}
|
|
947
|
+
} @catch (NSException *exception) {
|
|
948
|
+
NSLog(@"[AdvancedTextView] Exception in emitWordEvent: %@", exception);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
818
952
|
- (void)emitSelectionEvent:(NSString *)selectedText menuOption:(NSString *)option
|
|
819
953
|
{
|
|
820
954
|
NSLog(@"[AdvancedTextView] emitSelectionEvent: %@ with option: %@", selectedText, option);
|
|
@@ -822,9 +956,15 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
|
|
|
822
956
|
if (_eventEmitter) {
|
|
823
957
|
auto emitter = std::static_pointer_cast<const AdvancedTextViewEventEmitter>(_eventEmitter);
|
|
824
958
|
|
|
959
|
+
NSInteger firstWord = -1;
|
|
960
|
+
NSInteger lastWord = -1;
|
|
961
|
+
[self wordIndexesForRange:_textView.selectedRange first:&firstWord last:&lastWord];
|
|
962
|
+
|
|
825
963
|
AdvancedTextViewEventEmitter::OnSelection event;
|
|
826
964
|
event.selectedText = [selectedText UTF8String];
|
|
827
965
|
event.event = [option UTF8String];
|
|
966
|
+
event.startWordIndex = static_cast<int>(firstWord);
|
|
967
|
+
event.endWordIndex = static_cast<int>(lastWord);
|
|
828
968
|
|
|
829
969
|
emitter->onSelection(event);
|
|
830
970
|
}
|
|
@@ -9,6 +9,8 @@ export const AdvancedText = ({
|
|
|
9
9
|
highlightedWords,
|
|
10
10
|
menuOptions,
|
|
11
11
|
onWordPress,
|
|
12
|
+
onWordDoublePress,
|
|
13
|
+
onWordLongPress,
|
|
12
14
|
onSelection,
|
|
13
15
|
...restProps
|
|
14
16
|
}) => {
|
|
@@ -19,7 +21,12 @@ export const AdvancedText = ({
|
|
|
19
21
|
highlightedWords: highlightedWords,
|
|
20
22
|
menuOptions: menuOptions,
|
|
21
23
|
onWordPress: onWordPress,
|
|
24
|
+
onWordDoublePress: onWordDoublePress,
|
|
25
|
+
onWordLongPress: onWordLongPress,
|
|
22
26
|
onSelection: onSelection
|
|
27
|
+
// Only apps that handle double taps pay the single-tap delay.
|
|
28
|
+
,
|
|
29
|
+
wordDoublePressEnabled: !!onWordDoublePress
|
|
23
30
|
});
|
|
24
31
|
};
|
|
25
32
|
//# sourceMappingURL=AdvancedText.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["React","AdvancedTextViewNativeComponent","jsx","_jsx","AdvancedText","text","style","highlightedWords","menuOptions","onWordPress","onSelection","restProps"],"sourceRoot":"../../src","sources":["AdvancedText.tsx"],"mappings":";;AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,OAAOC,+BAA+B,MAE/B,mCAAmC;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAE3C,OAAO,MAAMC,YAAmC,GAAGA,CAAC;EAClDC,IAAI;EACJC,KAAK;EACLC,gBAAgB;EAChBC,WAAW;EACXC,WAAW;EACXC,WAAW;EACX,GAAGC;AACL,CAAC,KAAK;EACJ,
|
|
1
|
+
{"version":3,"names":["React","AdvancedTextViewNativeComponent","jsx","_jsx","AdvancedText","text","style","highlightedWords","menuOptions","onWordPress","onWordDoublePress","onWordLongPress","onSelection","restProps","wordDoublePressEnabled"],"sourceRoot":"../../src","sources":["AdvancedText.tsx"],"mappings":";;AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,OAAOC,+BAA+B,MAE/B,mCAAmC;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAE3C,OAAO,MAAMC,YAAmC,GAAGA,CAAC;EAClDC,IAAI;EACJC,KAAK;EACLC,gBAAgB;EAChBC,WAAW;EACXC,WAAW;EACXC,iBAAiB;EACjBC,eAAe;EACfC,WAAW;EACX,GAAGC;AACL,CAAC,KAAK;EACJ,oBACEV,IAAA,CAACF,+BAA+B;IAAA,GAC1BY,SAAS;IACbP,KAAK,EAAEA,KAAM;IACbD,IAAI,EAAEA,IAAK;IACXE,gBAAgB,EAAEA,gBAAiB;IACnCC,WAAW,EAAEA,WAAY;IACzBC,WAAW,EAAEA,WAAY;IACzBC,iBAAiB,EAAEA,iBAAkB;IACrCC,eAAe,EAAEA,eAAgB;IACjCC,WAAW,EAAEA;IACb;IAAA;IACAE,sBAAsB,EAAE,CAAC,CAACJ;EAAkB,CAC7C,CAAC;AAEN,CAAC","ignoreList":[]}
|
|
@@ -18,7 +18,21 @@ export interface NativeProps extends ViewProps {
|
|
|
18
18
|
highlightedWords?: ReadonlyArray<HighlightedWord>;
|
|
19
19
|
menuOptions?: ReadonlyArray<string>;
|
|
20
20
|
onWordPress?: DirectEventHandler<{ word: string; index: Int32 }>;
|
|
21
|
-
|
|
21
|
+
// A double tap on a word. It never selects text.
|
|
22
|
+
onWordDoublePress?: DirectEventHandler<{ word: string; index: Int32 }>;
|
|
23
|
+
// A long press on a word. Text selection still starts as usual.
|
|
24
|
+
onWordLongPress?: DirectEventHandler<{ word: string; index: Int32 }>;
|
|
25
|
+
// startWordIndex/endWordIndex: the first and last word the selection
|
|
26
|
+
// touches, or -1 when it touches none.
|
|
27
|
+
onSelection?: DirectEventHandler<{
|
|
28
|
+
selectedText: string;
|
|
29
|
+
event: string;
|
|
30
|
+
startWordIndex: Int32;
|
|
31
|
+
endWordIndex: Int32;
|
|
32
|
+
}>;
|
|
33
|
+
// Set by AdvancedText when onWordDoublePress is given. While on, a single
|
|
34
|
+
// tap waits out the double-tap delay before firing onWordPress.
|
|
35
|
+
wordDoublePressEnabled?: boolean;
|
|
22
36
|
fontSize?: Int32;
|
|
23
37
|
fontWeight?: string;
|
|
24
38
|
color?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdvancedText.d.ts","sourceRoot":"","sources":["../../../src/AdvancedText.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAwC,EACtC,KAAK,WAAW,EACjB,MAAM,mCAAmC,CAAC;AAE3C,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,
|
|
1
|
+
{"version":3,"file":"AdvancedText.d.ts","sourceRoot":"","sources":["../../../src/AdvancedText.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAwC,EACtC,KAAK,WAAW,EACjB,MAAM,mCAAmC,CAAC;AAE3C,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CA0B9C,CAAC"}
|
|
@@ -14,10 +14,21 @@ export interface NativeProps extends ViewProps {
|
|
|
14
14
|
word: string;
|
|
15
15
|
index: Int32;
|
|
16
16
|
}>;
|
|
17
|
+
onWordDoublePress?: DirectEventHandler<{
|
|
18
|
+
word: string;
|
|
19
|
+
index: Int32;
|
|
20
|
+
}>;
|
|
21
|
+
onWordLongPress?: DirectEventHandler<{
|
|
22
|
+
word: string;
|
|
23
|
+
index: Int32;
|
|
24
|
+
}>;
|
|
17
25
|
onSelection?: DirectEventHandler<{
|
|
18
26
|
selectedText: string;
|
|
19
27
|
event: string;
|
|
28
|
+
startWordIndex: Int32;
|
|
29
|
+
endWordIndex: Int32;
|
|
20
30
|
}>;
|
|
31
|
+
wordDoublePressEnabled?: boolean;
|
|
21
32
|
fontSize?: Int32;
|
|
22
33
|
fontWeight?: string;
|
|
23
34
|
color?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdvancedTextViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/AdvancedTextViewNativeComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAG,KAAK,EAAG,MAAM,2CAA2C,CAAC;AAEpG,UAAU,eAAe;IACvB,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,KAAK,CAAC;IAGrB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAClD,WAAW,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,kBAAkB,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"AdvancedTextViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/AdvancedTextViewNativeComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAG,KAAK,EAAG,MAAM,2CAA2C,CAAC;AAEpG,UAAU,eAAe;IACvB,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,KAAK,CAAC;IAGrB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAClD,WAAW,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,kBAAkB,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC;IAEjE,iBAAiB,CAAC,EAAE,kBAAkB,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC;IAEvE,eAAe,CAAC,EAAE,kBAAkB,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC;IAGrE,WAAW,CAAC,EAAE,kBAAkB,CAAC;QAC/B,YAAY,EAAE,MAAM,CAAC;QACrB,KAAK,EAAE,MAAM,CAAC;QACd,cAAc,EAAE,KAAK,CAAC;QACtB,YAAY,EAAE,KAAK,CAAC;KACrB,CAAC,CAAC;IAGH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,KAAK,CAAC;IACnB,aAAa,CAAC,EAAE,KAAK,CAAC;IACtB,kBAAkB,CAAC,EAAE,KAAK,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;;AAED,wBAAuE"}
|
package/package.json
CHANGED
package/src/AdvancedText.tsx
CHANGED
|
@@ -9,6 +9,8 @@ export const AdvancedText: React.FC<NativeProps> = ({
|
|
|
9
9
|
highlightedWords,
|
|
10
10
|
menuOptions,
|
|
11
11
|
onWordPress,
|
|
12
|
+
onWordDoublePress,
|
|
13
|
+
onWordLongPress,
|
|
12
14
|
onSelection,
|
|
13
15
|
...restProps
|
|
14
16
|
}) => {
|
|
@@ -20,7 +22,11 @@ export const AdvancedText: React.FC<NativeProps> = ({
|
|
|
20
22
|
highlightedWords={highlightedWords}
|
|
21
23
|
menuOptions={menuOptions}
|
|
22
24
|
onWordPress={onWordPress}
|
|
25
|
+
onWordDoublePress={onWordDoublePress}
|
|
26
|
+
onWordLongPress={onWordLongPress}
|
|
23
27
|
onSelection={onSelection}
|
|
28
|
+
// Only apps that handle double taps pay the single-tap delay.
|
|
29
|
+
wordDoublePressEnabled={!!onWordDoublePress}
|
|
24
30
|
/>
|
|
25
31
|
);
|
|
26
32
|
};
|
|
@@ -18,7 +18,21 @@ export interface NativeProps extends ViewProps {
|
|
|
18
18
|
highlightedWords?: ReadonlyArray<HighlightedWord>;
|
|
19
19
|
menuOptions?: ReadonlyArray<string>;
|
|
20
20
|
onWordPress?: DirectEventHandler<{ word: string; index: Int32 }>;
|
|
21
|
-
|
|
21
|
+
// A double tap on a word. It never selects text.
|
|
22
|
+
onWordDoublePress?: DirectEventHandler<{ word: string; index: Int32 }>;
|
|
23
|
+
// A long press on a word. Text selection still starts as usual.
|
|
24
|
+
onWordLongPress?: DirectEventHandler<{ word: string; index: Int32 }>;
|
|
25
|
+
// startWordIndex/endWordIndex: the first and last word the selection
|
|
26
|
+
// touches, or -1 when it touches none.
|
|
27
|
+
onSelection?: DirectEventHandler<{
|
|
28
|
+
selectedText: string;
|
|
29
|
+
event: string;
|
|
30
|
+
startWordIndex: Int32;
|
|
31
|
+
endWordIndex: Int32;
|
|
32
|
+
}>;
|
|
33
|
+
// Set by AdvancedText when onWordDoublePress is given. While on, a single
|
|
34
|
+
// tap waits out the double-tap delay before firing onWordPress.
|
|
35
|
+
wordDoublePressEnabled?: boolean;
|
|
22
36
|
fontSize?: Int32;
|
|
23
37
|
fontWeight?: string;
|
|
24
38
|
color?: string;
|