react-native-advanced-text 0.2.15 → 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 +21 -0
- package/android/src/main/java/com/advancedtext/AdvancedTextView.kt +209 -19
- package/android/src/main/java/com/advancedtext/AdvancedTextViewManager.kt +11 -1
- package/ios/AdvancedTextView.mm +174 -2
- package/lib/module/AdvancedText.js +7 -0
- package/lib/module/AdvancedText.js.map +1 -1
- package/lib/module/AdvancedTextViewNativeComponent.ts +18 -1
- package/lib/typescript/src/AdvancedText.d.ts.map +1 -1
- package/lib/typescript/src/AdvancedTextViewNativeComponent.d.ts +12 -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 +18 -1
package/README.md
CHANGED
|
@@ -38,6 +38,11 @@ import { AdvancedText } from "react-native-advanced-text";
|
|
|
38
38
|
highlightColor: '#6baeffb5',
|
|
39
39
|
borderRadius: 6,
|
|
40
40
|
},
|
|
41
|
+
{
|
|
42
|
+
index: 7,
|
|
43
|
+
highlightColor: '#818CF8',
|
|
44
|
+
underline: true, // a line under the word instead of a background
|
|
45
|
+
},
|
|
41
46
|
]}
|
|
42
47
|
fontSize={24}
|
|
43
48
|
color={'#FFFFFF'}
|
|
@@ -47,6 +52,22 @@ import { AdvancedText } from "react-native-advanced-text";
|
|
|
47
52
|
/>
|
|
48
53
|
```
|
|
49
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
|
+
|
|
64
|
+
## Underlined words
|
|
65
|
+
|
|
66
|
+
An entry in `highlightedWords` with `underline: true` draws a line under the
|
|
67
|
+
word in `highlightColor` instead of filling its background (`borderRadius` is
|
|
68
|
+
ignored). A word can be both highlighted and underlined: list it twice, once
|
|
69
|
+
with `underline: true` and once without.
|
|
70
|
+
|
|
50
71
|
## Sizing
|
|
51
72
|
|
|
52
73
|
`AdvancedText` measures its own height, like React Native's built‑in `<Text>`.
|
|
@@ -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"
|
|
@@ -66,6 +69,24 @@ class AdvancedTextView : TextView {
|
|
|
66
69
|
private val rangePath = Path()
|
|
67
70
|
private val lineClipPath = Path()
|
|
68
71
|
private val rangeBounds = RectF()
|
|
72
|
+
private val underlineBounds = RectF()
|
|
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
|
|
69
90
|
|
|
70
91
|
constructor(context: Context?) : super(context) { init() }
|
|
71
92
|
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { init() }
|
|
@@ -92,6 +113,19 @@ class AdvancedTextView : TextView {
|
|
|
92
113
|
|
|
93
114
|
movementMethod = SmartMovementMethod
|
|
94
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
|
+
|
|
95
129
|
customSelectionActionModeCallback = object : ActionMode.Callback {
|
|
96
130
|
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
|
97
131
|
customActionMode = mode
|
|
@@ -106,6 +140,7 @@ class AdvancedTextView : TextView {
|
|
|
106
140
|
|
|
107
141
|
if (selectionStart >= 0 && selectionEnd >= 0 && selectionStart != selectionEnd) {
|
|
108
142
|
lastSelectedText = text.subSequence(selectionStart, selectionEnd).toString()
|
|
143
|
+
lastSelectedWords = wordIndexRange(selectionStart, selectionEnd)
|
|
109
144
|
|
|
110
145
|
menuOptions.forEachIndexed { index, option ->
|
|
111
146
|
menu?.add(0, index, index, option)
|
|
@@ -394,14 +429,25 @@ class AdvancedTextView : TextView {
|
|
|
394
429
|
|
|
395
430
|
highlightedWords.forEach { highlightedWord ->
|
|
396
431
|
val wordPos = wordPositions.find { it.index == highlightedWord.index } ?: return@forEach
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
432
|
+
if (highlightedWord.underline) {
|
|
433
|
+
// The word, but not the space after it.
|
|
434
|
+
drawRangeUnderline(
|
|
435
|
+
canvas,
|
|
436
|
+
currentLayout,
|
|
437
|
+
wordPos.start,
|
|
438
|
+
wordPos.end.coerceAtMost(currentText.length),
|
|
439
|
+
parseColor(highlightedWord.highlightColor)
|
|
440
|
+
)
|
|
441
|
+
} else {
|
|
442
|
+
drawRangeBackground(
|
|
443
|
+
canvas,
|
|
444
|
+
currentLayout,
|
|
445
|
+
wordPos.start,
|
|
446
|
+
wordPos.extendedEnd.coerceAtMost(currentText.length),
|
|
447
|
+
parseColor(highlightedWord.highlightColor),
|
|
448
|
+
highlightedWord.borderRadius * density
|
|
449
|
+
)
|
|
450
|
+
}
|
|
405
451
|
}
|
|
406
452
|
|
|
407
453
|
// Draw indicator background only in Highlight mode
|
|
@@ -420,10 +466,7 @@ class AdvancedTextView : TextView {
|
|
|
420
466
|
}
|
|
421
467
|
}
|
|
422
468
|
|
|
423
|
-
// Fills the background of text[start, end), one box per line.
|
|
424
|
-
// come from Layout.getSelectionPath, which follows the text's direction:
|
|
425
|
-
// a right-to-left word (Arabic) starts at its right edge, so treating
|
|
426
|
-
// getPrimaryHorizontal(start) as the left edge drew boxes in the wrong place.
|
|
469
|
+
// Fills the background of text[start, end), one box per line.
|
|
427
470
|
private fun drawRangeBackground(
|
|
428
471
|
canvas: Canvas,
|
|
429
472
|
layout: Layout,
|
|
@@ -432,8 +475,44 @@ class AdvancedTextView : TextView {
|
|
|
432
475
|
color: Int,
|
|
433
476
|
radius: Float
|
|
434
477
|
) {
|
|
435
|
-
if (start >= end) return
|
|
436
478
|
highlightPaint.color = color
|
|
479
|
+
forEachLineOfRange(layout, start, end) {
|
|
480
|
+
canvas.drawRoundRect(rangeBounds, radius, radius, highlightPaint)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Draws a line under text[start, end), one per line it spans, just below
|
|
485
|
+
// the baseline like a text underline.
|
|
486
|
+
private fun drawRangeUnderline(
|
|
487
|
+
canvas: Canvas,
|
|
488
|
+
layout: Layout,
|
|
489
|
+
start: Int,
|
|
490
|
+
end: Int,
|
|
491
|
+
color: Int
|
|
492
|
+
) {
|
|
493
|
+
val density = resources.displayMetrics.density
|
|
494
|
+
val thickness = UNDERLINE_THICKNESS_DP * density
|
|
495
|
+
val gap = UNDERLINE_GAP_DP * density
|
|
496
|
+
highlightPaint.color = color
|
|
497
|
+
forEachLineOfRange(layout, start, end) { line ->
|
|
498
|
+
val top = layout.getLineBaseline(line) + totalPaddingTop + gap
|
|
499
|
+
underlineBounds.set(rangeBounds.left, top, rangeBounds.right, top + thickness)
|
|
500
|
+
canvas.drawRoundRect(underlineBounds, thickness / 2, thickness / 2, highlightPaint)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Calls draw for each line text[start, end) spans, with rangeBounds set to
|
|
505
|
+
// that line's part of the range in view coordinates. The edges come from
|
|
506
|
+
// Layout.getSelectionPath, which follows the text's direction: a
|
|
507
|
+
// right-to-left word (Arabic) starts at its right edge, so treating
|
|
508
|
+
// getPrimaryHorizontal(start) as the left edge put boxes in the wrong place.
|
|
509
|
+
private inline fun forEachLineOfRange(
|
|
510
|
+
layout: Layout,
|
|
511
|
+
start: Int,
|
|
512
|
+
end: Int,
|
|
513
|
+
draw: (line: Int) -> Unit
|
|
514
|
+
) {
|
|
515
|
+
if (start >= end) return
|
|
437
516
|
|
|
438
517
|
val startLine = layout.getLineForOffset(start)
|
|
439
518
|
val endLine = layout.getLineForOffset(end - 1)
|
|
@@ -461,7 +540,7 @@ class AdvancedTextView : TextView {
|
|
|
461
540
|
if (rangeBounds.isEmpty) continue
|
|
462
541
|
|
|
463
542
|
rangeBounds.offset(totalPaddingLeft.toFloat(), totalPaddingTop.toFloat())
|
|
464
|
-
|
|
543
|
+
draw(line)
|
|
465
544
|
}
|
|
466
545
|
}
|
|
467
546
|
|
|
@@ -480,6 +559,8 @@ class AdvancedTextView : TextView {
|
|
|
480
559
|
val event = Arguments.createMap().apply {
|
|
481
560
|
putString("selectedText", selectedText)
|
|
482
561
|
putString("event", eventType)
|
|
562
|
+
putInt("startWordIndex", lastSelectedWords.first)
|
|
563
|
+
putInt("endWordIndex", lastSelectedWords.second)
|
|
483
564
|
}
|
|
484
565
|
|
|
485
566
|
reactContext.getJSModule(RCTEventEmitter::class.java)
|
|
@@ -496,7 +577,18 @@ class AdvancedTextView : TextView {
|
|
|
496
577
|
|
|
497
578
|
override fun onClick(widget: View) {
|
|
498
579
|
Log.d(TAG, "Word clicked: '$word' (index=$wordIndex)")
|
|
499
|
-
|
|
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)
|
|
500
592
|
}
|
|
501
593
|
|
|
502
594
|
// Skips super, which would paint the word in the link color and
|
|
@@ -506,7 +598,8 @@ class AdvancedTextView : TextView {
|
|
|
506
598
|
}
|
|
507
599
|
}
|
|
508
600
|
|
|
509
|
-
|
|
601
|
+
// onWordPress, onWordDoublePress or onWordLongPress.
|
|
602
|
+
private fun sendWordEvent(eventName: String, word: String, index: Int) {
|
|
510
603
|
try {
|
|
511
604
|
val reactContext = context as? ReactContext ?: return
|
|
512
605
|
val event = Arguments.createMap().apply {
|
|
@@ -515,10 +608,102 @@ class AdvancedTextView : TextView {
|
|
|
515
608
|
}
|
|
516
609
|
|
|
517
610
|
reactContext.getJSModule(RCTEventEmitter::class.java)
|
|
518
|
-
.receiveEvent(id,
|
|
611
|
+
.receiveEvent(id, eventName, event)
|
|
519
612
|
} catch (e: Exception) {
|
|
520
|
-
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
|
+
}
|
|
521
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
|
+
}
|
|
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
|
|
522
707
|
}
|
|
523
708
|
|
|
524
709
|
fun clearSelection() {
|
|
@@ -589,5 +774,10 @@ class AdvancedTextView : TextView {
|
|
|
589
774
|
data class HighlightedWord(
|
|
590
775
|
val index: Int,
|
|
591
776
|
val highlightColor: String,
|
|
592
|
-
val borderRadius: Float = 0f
|
|
777
|
+
val borderRadius: Float = 0f,
|
|
778
|
+
// Draws a line under the word instead of filling its background.
|
|
779
|
+
val underline: Boolean = false
|
|
593
780
|
)
|
|
781
|
+
|
|
782
|
+
private const val UNDERLINE_THICKNESS_DP = 1.5f
|
|
783
|
+
private const val UNDERLINE_GAP_DP = 2.5f
|
|
@@ -54,7 +54,10 @@ class AdvancedTextViewManager : SimpleViewManager<AdvancedTextView>() {
|
|
|
54
54
|
wordMap.getDouble("borderRadius").toFloat()
|
|
55
55
|
} else {
|
|
56
56
|
0f
|
|
57
|
-
}
|
|
57
|
+
},
|
|
58
|
+
underline = wordMap.hasKey("underline") &&
|
|
59
|
+
!wordMap.isNull("underline") &&
|
|
60
|
+
wordMap.getBoolean("underline")
|
|
58
61
|
)
|
|
59
62
|
)
|
|
60
63
|
}
|
|
@@ -158,9 +161,16 @@ class AdvancedTextViewManager : SimpleViewManager<AdvancedTextView>() {
|
|
|
158
161
|
view?.setAdvancedLetterSpacing(letterSpacing)
|
|
159
162
|
}
|
|
160
163
|
|
|
164
|
+
@ReactProp(name = "wordDoublePressEnabled")
|
|
165
|
+
fun setWordDoublePressEnabled(view: AdvancedTextView?, enabled: Boolean) {
|
|
166
|
+
view?.setWordDoublePressEnabled(enabled)
|
|
167
|
+
}
|
|
168
|
+
|
|
161
169
|
override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any> {
|
|
162
170
|
return mapOf(
|
|
163
171
|
"onWordPress" to mapOf("registrationName" to "onWordPress"),
|
|
172
|
+
"onWordDoublePress" to mapOf("registrationName" to "onWordDoublePress"),
|
|
173
|
+
"onWordLongPress" to mapOf("registrationName" to "onWordLongPress"),
|
|
164
174
|
"onSelection" to mapOf("registrationName" to "onSelection")
|
|
165
175
|
)
|
|
166
176
|
}
|
package/ios/AdvancedTextView.mm
CHANGED
|
@@ -31,6 +31,9 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
|
|
|
31
31
|
|
|
32
32
|
@property (nonatomic, strong) NSMutableArray<NSDictionary *> *wordRanges;
|
|
33
33
|
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, NSDictionary *> *highlightStyles;
|
|
34
|
+
// Underline colors by word index, kept apart from highlightStyles so a word
|
|
35
|
+
// can be both highlighted and underlined.
|
|
36
|
+
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, UIColor *> *underlineStyles;
|
|
34
37
|
@property (nonatomic, strong) NSArray<NSString *> *menuOptions;
|
|
35
38
|
@property (nonatomic, assign) CGFloat fontSize;
|
|
36
39
|
@property (nonatomic, strong) NSString *fontWeight;
|
|
@@ -88,6 +91,11 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
|
|
|
88
91
|
@interface AdvancedTextView () <RCTAdvancedTextViewViewProtocol, UIGestureRecognizerDelegate, UITextViewDelegate>
|
|
89
92
|
|
|
90
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;
|
|
91
99
|
|
|
92
100
|
@end
|
|
93
101
|
|
|
@@ -109,6 +117,7 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
|
|
|
109
117
|
|
|
110
118
|
_wordRanges = [NSMutableArray array];
|
|
111
119
|
_highlightStyles = [NSMutableDictionary dictionary];
|
|
120
|
+
_underlineStyles = [NSMutableDictionary dictionary];
|
|
112
121
|
_fontSize = 16.0;
|
|
113
122
|
_fontWeight = @"normal";
|
|
114
123
|
_textColor = [UIColor labelColor];
|
|
@@ -173,6 +182,24 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
|
|
|
173
182
|
action:@selector(handleTap:)];
|
|
174
183
|
tapGesture.delegate = self;
|
|
175
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;
|
|
176
203
|
|
|
177
204
|
NSLog(@"[AdvancedTextView] Gesture recognizers setup successful");
|
|
178
205
|
} @catch (NSException *exception) {
|
|
@@ -224,6 +251,11 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
|
|
|
224
251
|
styleChanged = YES;
|
|
225
252
|
}
|
|
226
253
|
|
|
254
|
+
if (oldViewProps.wordDoublePressEnabled != newViewProps.wordDoublePressEnabled) {
|
|
255
|
+
_wordDoublePressEnabled = newViewProps.wordDoublePressEnabled;
|
|
256
|
+
_doubleTapGesture.enabled = _wordDoublePressEnabled;
|
|
257
|
+
}
|
|
258
|
+
|
|
227
259
|
if (oldViewProps.text != newViewProps.text) {
|
|
228
260
|
textChanged = YES;
|
|
229
261
|
NSLog(@"[AdvancedTextView] Text changed");
|
|
@@ -237,7 +269,8 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
|
|
|
237
269
|
const auto &newHW = newViewProps.highlightedWords[i];
|
|
238
270
|
if (oldHW.index != newHW.index ||
|
|
239
271
|
oldHW.highlightColor != newHW.highlightColor ||
|
|
240
|
-
oldHW.borderRadius != newHW.borderRadius
|
|
272
|
+
oldHW.borderRadius != newHW.borderRadius ||
|
|
273
|
+
oldHW.underline != newHW.underline) {
|
|
241
274
|
highlightsChanged = YES;
|
|
242
275
|
break;
|
|
243
276
|
}
|
|
@@ -410,6 +443,7 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
|
|
|
410
443
|
highlightedWords.size());
|
|
411
444
|
@try {
|
|
412
445
|
[_highlightStyles removeAllObjects];
|
|
446
|
+
[_underlineStyles removeAllObjects];
|
|
413
447
|
|
|
414
448
|
for (const auto &hw : highlightedWords) {
|
|
415
449
|
NSInteger index = hw.index;
|
|
@@ -417,7 +451,9 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
|
|
|
417
451
|
UIColor *color = [self hexStringToColor:colorString];
|
|
418
452
|
CGFloat borderRadius = static_cast<CGFloat>(hw.borderRadius);
|
|
419
453
|
|
|
420
|
-
if (color) {
|
|
454
|
+
if (color && hw.underline) {
|
|
455
|
+
_underlineStyles[@(index)] = color;
|
|
456
|
+
} else if (color) {
|
|
421
457
|
_highlightStyles[@(index)] = @{
|
|
422
458
|
@"color": color,
|
|
423
459
|
@"borderRadius": @(MAX(borderRadius, 0.0))
|
|
@@ -499,6 +535,7 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
|
|
|
499
535
|
|
|
500
536
|
|
|
501
537
|
[self applyWordColorsToString:attributedString];
|
|
538
|
+
[self applyUnderlinesToString:attributedString];
|
|
502
539
|
|
|
503
540
|
_textView.attributedText = attributedString;
|
|
504
541
|
[_textView setNeedsDisplay];
|
|
@@ -545,6 +582,29 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
|
|
|
545
582
|
}
|
|
546
583
|
}
|
|
547
584
|
|
|
585
|
+
// A text underline under each underlined word, but not the space after it.
|
|
586
|
+
- (void)applyUnderlinesToString:(NSMutableAttributedString *)string
|
|
587
|
+
{
|
|
588
|
+
if (_underlineStyles.count == 0) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
for (NSDictionary *wordInfo in _wordRanges) {
|
|
593
|
+
UIColor *color = _underlineStyles[wordInfo[@"index"]];
|
|
594
|
+
if (!color) {
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
NSRange wordRange = [wordInfo[@"range"] rangeValue];
|
|
598
|
+
if (NSMaxRange(wordRange) > string.length) {
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
[string addAttributes:@{
|
|
602
|
+
NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle),
|
|
603
|
+
NSUnderlineColorAttributeName: color
|
|
604
|
+
} range:wordRange];
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
548
608
|
// Recolors the live textStorage instead of reassigning attributedText, so the
|
|
549
609
|
// indicator can advance while the user is selecting text: attribute-only
|
|
550
610
|
// edits keep selectedRange and the edit menu, assigning attributedText resets them.
|
|
@@ -661,6 +721,89 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
|
|
|
661
721
|
}
|
|
662
722
|
}
|
|
663
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
|
+
|
|
664
807
|
#pragma mark - UITextViewDelegate
|
|
665
808
|
|
|
666
809
|
- (void)textViewDidChangeSelection:(UITextView *)textView
|
|
@@ -783,6 +926,29 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
|
|
|
783
926
|
}
|
|
784
927
|
}
|
|
785
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
|
+
|
|
786
952
|
- (void)emitSelectionEvent:(NSString *)selectedText menuOption:(NSString *)option
|
|
787
953
|
{
|
|
788
954
|
NSLog(@"[AdvancedTextView] emitSelectionEvent: %@ with option: %@", selectedText, option);
|
|
@@ -790,9 +956,15 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
|
|
|
790
956
|
if (_eventEmitter) {
|
|
791
957
|
auto emitter = std::static_pointer_cast<const AdvancedTextViewEventEmitter>(_eventEmitter);
|
|
792
958
|
|
|
959
|
+
NSInteger firstWord = -1;
|
|
960
|
+
NSInteger lastWord = -1;
|
|
961
|
+
[self wordIndexesForRange:_textView.selectedRange first:&firstWord last:&lastWord];
|
|
962
|
+
|
|
793
963
|
AdvancedTextViewEventEmitter::OnSelection event;
|
|
794
964
|
event.selectedText = [selectedText UTF8String];
|
|
795
965
|
event.event = [option UTF8String];
|
|
966
|
+
event.startWordIndex = static_cast<int>(firstWord);
|
|
967
|
+
event.endWordIndex = static_cast<int>(lastWord);
|
|
796
968
|
|
|
797
969
|
emitter->onSelection(event);
|
|
798
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":[]}
|
|
@@ -8,6 +8,9 @@ interface HighlightedWord {
|
|
|
8
8
|
index: Int32;
|
|
9
9
|
highlightColor: string;
|
|
10
10
|
borderRadius?: Float;
|
|
11
|
+
// Underlines the word in highlightColor instead of filling its background.
|
|
12
|
+
// A word can have both: list it once with and once without.
|
|
13
|
+
underline?: boolean;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export interface NativeProps extends ViewProps {
|
|
@@ -15,7 +18,21 @@ export interface NativeProps extends ViewProps {
|
|
|
15
18
|
highlightedWords?: ReadonlyArray<HighlightedWord>;
|
|
16
19
|
menuOptions?: ReadonlyArray<string>;
|
|
17
20
|
onWordPress?: DirectEventHandler<{ word: string; index: Int32 }>;
|
|
18
|
-
|
|
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;
|
|
19
36
|
fontSize?: Int32;
|
|
20
37
|
fontWeight?: string;
|
|
21
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"}
|
|
@@ -4,6 +4,7 @@ interface HighlightedWord {
|
|
|
4
4
|
index: Int32;
|
|
5
5
|
highlightColor: string;
|
|
6
6
|
borderRadius?: Float;
|
|
7
|
+
underline?: boolean;
|
|
7
8
|
}
|
|
8
9
|
export interface NativeProps extends ViewProps {
|
|
9
10
|
text: string;
|
|
@@ -13,10 +14,21 @@ export interface NativeProps extends ViewProps {
|
|
|
13
14
|
word: string;
|
|
14
15
|
index: Int32;
|
|
15
16
|
}>;
|
|
17
|
+
onWordDoublePress?: DirectEventHandler<{
|
|
18
|
+
word: string;
|
|
19
|
+
index: Int32;
|
|
20
|
+
}>;
|
|
21
|
+
onWordLongPress?: DirectEventHandler<{
|
|
22
|
+
word: string;
|
|
23
|
+
index: Int32;
|
|
24
|
+
}>;
|
|
16
25
|
onSelection?: DirectEventHandler<{
|
|
17
26
|
selectedText: string;
|
|
18
27
|
event: string;
|
|
28
|
+
startWordIndex: Int32;
|
|
29
|
+
endWordIndex: Int32;
|
|
19
30
|
}>;
|
|
31
|
+
wordDoublePressEnabled?: boolean;
|
|
20
32
|
fontSize?: Int32;
|
|
21
33
|
fontWeight?: string;
|
|
22
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;
|
|
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
|
};
|
|
@@ -8,6 +8,9 @@ interface HighlightedWord {
|
|
|
8
8
|
index: Int32;
|
|
9
9
|
highlightColor: string;
|
|
10
10
|
borderRadius?: Float;
|
|
11
|
+
// Underlines the word in highlightColor instead of filling its background.
|
|
12
|
+
// A word can have both: list it once with and once without.
|
|
13
|
+
underline?: boolean;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export interface NativeProps extends ViewProps {
|
|
@@ -15,7 +18,21 @@ export interface NativeProps extends ViewProps {
|
|
|
15
18
|
highlightedWords?: ReadonlyArray<HighlightedWord>;
|
|
16
19
|
menuOptions?: ReadonlyArray<string>;
|
|
17
20
|
onWordPress?: DirectEventHandler<{ word: string; index: Int32 }>;
|
|
18
|
-
|
|
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;
|
|
19
36
|
fontSize?: Int32;
|
|
20
37
|
fontWeight?: string;
|
|
21
38
|
color?: string;
|