react-native-enriched 0.4.0 → 0.5.0
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 +27 -2
- package/ReactNativeEnriched.podspec +5 -1
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +12 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +4 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/EventEmitters.cpp +149 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/EventEmitters.h +146 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/Props.cpp +16 -1
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/Props.h +46 -0
- package/android/src/main/java/com/swmansion/enriched/common/GumboNormalizer.kt +5 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedCheckboxListSpan.kt +3 -2
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedUnorderedListSpan.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt +166 -20
- package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt +32 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/MeasurementStore.kt +19 -2
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnContextMenuItemPressEvent.kt +35 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedLineHeightSpan.kt +44 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/styles/ParagraphStyles.kt +19 -14
- package/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt +16 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpanState.kt +18 -12
- package/android/src/main/new_arch/CMakeLists.txt +9 -13
- package/android/src/main/new_arch/GumboNormalizerJni.cpp +14 -0
- package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/conversions.h +2 -21
- package/cpp/CMakeLists.txt +50 -0
- package/cpp/GumboParser/GumboParser.h +34043 -0
- package/cpp/README.md +59 -0
- package/cpp/parser/GumboNormalizer.c +915 -0
- package/cpp/parser/GumboParser.cpp +16 -0
- package/cpp/parser/GumboParser.hpp +23 -0
- package/cpp/tests/GumboParserTest.cpp +457 -0
- package/ios/EnrichedTextInputView.h +2 -0
- package/ios/EnrichedTextInputView.mm +160 -2
- package/ios/config/InputConfig.h +3 -0
- package/ios/config/InputConfig.mm +15 -0
- package/ios/extensions/LayoutManagerExtension.mm +34 -11
- package/ios/generated/ReactNativeEnrichedSpec/EventEmitters.cpp +149 -0
- package/ios/generated/ReactNativeEnrichedSpec/EventEmitters.h +146 -0
- package/ios/generated/ReactNativeEnrichedSpec/Props.cpp +16 -1
- package/ios/generated/ReactNativeEnrichedSpec/Props.h +46 -0
- package/ios/generated/ReactNativeEnrichedSpec/RCTComponentViewHelpers.h +29 -0
- package/ios/inputParser/InputParser.mm +27 -0
- package/ios/interfaces/ImageAttachment.mm +29 -0
- package/lib/module/EnrichedTextInput.js +43 -30
- package/lib/module/EnrichedTextInput.js.map +1 -1
- package/lib/module/spec/EnrichedTextInputNativeComponent.ts +118 -22
- package/lib/typescript/src/EnrichedTextInput.d.ts +24 -6
- package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/spec/EnrichedTextInputNativeComponent.d.ts +111 -21
- package/lib/typescript/src/spec/EnrichedTextInputNativeComponent.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/EnrichedTextInput.tsx +79 -40
- package/src/index.tsx +0 -1
- package/src/spec/EnrichedTextInputNativeComponent.ts +118 -22
|
@@ -15,16 +15,18 @@ import android.util.AttributeSet
|
|
|
15
15
|
import android.util.Log
|
|
16
16
|
import android.util.Patterns
|
|
17
17
|
import android.util.TypedValue
|
|
18
|
+
import android.view.ActionMode
|
|
18
19
|
import android.view.Gravity
|
|
20
|
+
import android.view.Menu
|
|
21
|
+
import android.view.MenuItem
|
|
19
22
|
import android.view.MotionEvent
|
|
20
23
|
import android.view.inputmethod.EditorInfo
|
|
21
24
|
import android.view.inputmethod.InputConnection
|
|
22
25
|
import android.view.inputmethod.InputMethodManager
|
|
23
26
|
import androidx.appcompat.widget.AppCompatEditText
|
|
24
27
|
import androidx.core.view.ViewCompat
|
|
25
|
-
import androidx.core.view.inputmethod.EditorInfoCompat
|
|
26
|
-
import androidx.core.view.inputmethod.InputConnectionCompat
|
|
27
28
|
import com.facebook.react.bridge.ReactContext
|
|
29
|
+
import com.facebook.react.bridge.ReadableArray
|
|
28
30
|
import com.facebook.react.bridge.ReadableMap
|
|
29
31
|
import com.facebook.react.common.ReactConstants
|
|
30
32
|
import com.facebook.react.uimanager.PixelUtil
|
|
@@ -34,8 +36,10 @@ import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
|
|
|
34
36
|
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
|
|
35
37
|
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
|
|
36
38
|
import com.swmansion.enriched.common.EnrichedConstants
|
|
39
|
+
import com.swmansion.enriched.common.GumboNormalizer
|
|
37
40
|
import com.swmansion.enriched.common.parser.EnrichedParser
|
|
38
41
|
import com.swmansion.enriched.textinput.events.MentionHandler
|
|
42
|
+
import com.swmansion.enriched.textinput.events.OnContextMenuItemPressEvent
|
|
39
43
|
import com.swmansion.enriched.textinput.events.OnInputBlurEvent
|
|
40
44
|
import com.swmansion.enriched.textinput.events.OnInputFocusEvent
|
|
41
45
|
import com.swmansion.enriched.textinput.events.OnRequestHtmlResultEvent
|
|
@@ -46,6 +50,8 @@ import com.swmansion.enriched.textinput.spans.EnrichedInputH4Span
|
|
|
46
50
|
import com.swmansion.enriched.textinput.spans.EnrichedInputH5Span
|
|
47
51
|
import com.swmansion.enriched.textinput.spans.EnrichedInputH6Span
|
|
48
52
|
import com.swmansion.enriched.textinput.spans.EnrichedInputImageSpan
|
|
53
|
+
import com.swmansion.enriched.textinput.spans.EnrichedInputLinkSpan
|
|
54
|
+
import com.swmansion.enriched.textinput.spans.EnrichedLineHeightSpan
|
|
49
55
|
import com.swmansion.enriched.textinput.spans.EnrichedSpans
|
|
50
56
|
import com.swmansion.enriched.textinput.spans.interfaces.EnrichedInputSpan
|
|
51
57
|
import com.swmansion.enriched.textinput.styles.HtmlStyle
|
|
@@ -94,8 +100,10 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
94
100
|
var shouldEmitHtml: Boolean = false
|
|
95
101
|
var shouldEmitOnChangeText: Boolean = false
|
|
96
102
|
var experimentalSynchronousEvents: Boolean = false
|
|
103
|
+
var useHtmlNormalizer: Boolean = false
|
|
97
104
|
|
|
98
105
|
var fontSize: Float? = null
|
|
106
|
+
private var lineHeight: Float? = null
|
|
99
107
|
private var autoFocus = false
|
|
100
108
|
private var typefaceDirty = false
|
|
101
109
|
private var didAttachToWindow = false
|
|
@@ -108,6 +116,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
108
116
|
|
|
109
117
|
private var inputMethodManager: InputMethodManager? = null
|
|
110
118
|
private val spannableFactory = EnrichedTextInputSpannableFactory()
|
|
119
|
+
private var contextMenuItems: List<Pair<Int, String>> = emptyList()
|
|
111
120
|
|
|
112
121
|
constructor(context: Context) : super(context) {
|
|
113
122
|
prepareComponent()
|
|
@@ -271,7 +280,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
271
280
|
val parsedText = parseText(htmlText)
|
|
272
281
|
if (parsedText is Spannable) {
|
|
273
282
|
val finalText = currentText.mergeSpannables(start, end, parsedText)
|
|
274
|
-
setValue(finalText)
|
|
283
|
+
setValue(finalText, false)
|
|
275
284
|
return
|
|
276
285
|
}
|
|
277
286
|
}
|
|
@@ -292,26 +301,45 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
292
301
|
setSelection(selection?.start ?: text?.length ?: 0)
|
|
293
302
|
}
|
|
294
303
|
|
|
295
|
-
private fun
|
|
296
|
-
|
|
297
|
-
|
|
304
|
+
private fun normalizeHtmlIfNeeded(text: CharSequence): CharSequence {
|
|
305
|
+
if (!useHtmlNormalizer) return text
|
|
306
|
+
val normalized = GumboNormalizer.normalizeHtml(text.toString()) ?: return text
|
|
298
307
|
|
|
299
|
-
try {
|
|
300
|
-
val parsed = EnrichedParser.fromHtml(
|
|
301
|
-
|
|
302
|
-
return withoutLastNewLine
|
|
308
|
+
return try {
|
|
309
|
+
val parsed = EnrichedParser.fromHtml(normalized, htmlStyle, spannableFactory)
|
|
310
|
+
parsed.trimEnd('\n')
|
|
303
311
|
} catch (e: Exception) {
|
|
304
|
-
Log.e(
|
|
305
|
-
|
|
312
|
+
Log.e(TAG, "Error parsing normalized HTML: ${e.message}")
|
|
313
|
+
text
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private fun parseText(text: CharSequence): CharSequence {
|
|
318
|
+
val isInternalHtml = text.startsWith("<html>") && text.endsWith("</html>")
|
|
319
|
+
|
|
320
|
+
if (isInternalHtml) {
|
|
321
|
+
try {
|
|
322
|
+
val parsed = EnrichedParser.fromHtml(text.toString(), htmlStyle, spannableFactory)
|
|
323
|
+
return parsed.trimEnd('\n')
|
|
324
|
+
} catch (e: Exception) {
|
|
325
|
+
Log.e(TAG, "Error parsing HTML: ${e.message}")
|
|
326
|
+
return normalizeHtmlIfNeeded(text)
|
|
327
|
+
}
|
|
306
328
|
}
|
|
329
|
+
|
|
330
|
+
return normalizeHtmlIfNeeded(text)
|
|
307
331
|
}
|
|
308
332
|
|
|
309
|
-
fun setValue(
|
|
333
|
+
fun setValue(
|
|
334
|
+
value: CharSequence?,
|
|
335
|
+
shouldParseHtml: Boolean = true,
|
|
336
|
+
) {
|
|
310
337
|
if (value == null) return
|
|
311
338
|
|
|
312
339
|
runAsATransaction {
|
|
313
|
-
val newText = parseText(value)
|
|
340
|
+
val newText = if (shouldParseHtml) parseText(value) else value
|
|
314
341
|
setText(newText)
|
|
342
|
+
applyLineSpacing()
|
|
315
343
|
|
|
316
344
|
observeAsyncImages()
|
|
317
345
|
|
|
@@ -389,8 +417,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
389
417
|
|
|
390
418
|
fun setCursorColor(colorInt: Int?) {
|
|
391
419
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
392
|
-
val cursorDrawable = textCursorDrawable
|
|
393
|
-
if (cursorDrawable == null) return
|
|
420
|
+
val cursorDrawable = textCursorDrawable ?: return
|
|
394
421
|
|
|
395
422
|
if (colorInt != null) {
|
|
396
423
|
cursorDrawable.colorFilter = BlendModeColorFilter(colorInt, BlendMode.SRC_IN)
|
|
@@ -424,6 +451,28 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
424
451
|
forceScrollToSelection()
|
|
425
452
|
}
|
|
426
453
|
|
|
454
|
+
fun setLineHeight(height: Float) {
|
|
455
|
+
lineHeight = if (height == 0f) null else height
|
|
456
|
+
applyLineSpacing()
|
|
457
|
+
layoutManager.invalidateLayout()
|
|
458
|
+
forceScrollToSelection()
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private fun applyLineSpacing() {
|
|
462
|
+
val spannable = text as? Spannable ?: return
|
|
463
|
+
spannable
|
|
464
|
+
.getSpans(0, spannable.length, EnrichedLineHeightSpan::class.java)
|
|
465
|
+
.forEach { spannable.removeSpan(it) }
|
|
466
|
+
|
|
467
|
+
val lh = lineHeight ?: return
|
|
468
|
+
spannable.setSpan(
|
|
469
|
+
EnrichedLineHeightSpan(lh),
|
|
470
|
+
0,
|
|
471
|
+
spannable.length,
|
|
472
|
+
Spannable.SPAN_INCLUSIVE_INCLUSIVE,
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
|
|
427
476
|
fun setFontFamily(family: String?) {
|
|
428
477
|
if (family != fontFamily) {
|
|
429
478
|
fontFamily = family
|
|
@@ -490,12 +539,100 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
490
539
|
|
|
491
540
|
try {
|
|
492
541
|
linkRegex = Pattern.compile("(?s).*?($patternStr).*", flags)
|
|
493
|
-
} catch (
|
|
494
|
-
Log.w(
|
|
542
|
+
} catch (_: PatternSyntaxException) {
|
|
543
|
+
Log.w(TAG, "Invalid link regex pattern: $patternStr")
|
|
495
544
|
linkRegex = Patterns.WEB_URL
|
|
496
545
|
}
|
|
497
546
|
}
|
|
498
547
|
|
|
548
|
+
fun setContextMenuItems(items: ReadableArray?) {
|
|
549
|
+
if (items == null) {
|
|
550
|
+
contextMenuItems = emptyList()
|
|
551
|
+
return
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
val result = mutableListOf<Pair<Int, String>>()
|
|
555
|
+
for (i in 0 until items.size()) {
|
|
556
|
+
val item = items.getMap(i) ?: continue
|
|
557
|
+
val text = item.getString("text") ?: continue
|
|
558
|
+
result.add(Pair(i, text))
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
contextMenuItems = result
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
override fun startActionMode(
|
|
565
|
+
callback: ActionMode.Callback?,
|
|
566
|
+
type: Int,
|
|
567
|
+
): ActionMode? {
|
|
568
|
+
if (contextMenuItems.isEmpty()) {
|
|
569
|
+
return super.startActionMode(callback, type)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
val wrappedCallback =
|
|
573
|
+
object : ActionMode.Callback2() {
|
|
574
|
+
override fun onCreateActionMode(
|
|
575
|
+
mode: ActionMode,
|
|
576
|
+
menu: Menu,
|
|
577
|
+
): Boolean {
|
|
578
|
+
val result = callback?.onCreateActionMode(mode, menu) ?: false
|
|
579
|
+
for ((index, text) in contextMenuItems) {
|
|
580
|
+
menu.add(Menu.NONE, CONTEXT_MENU_ITEM_ID + index, Menu.NONE, text)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return result
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
override fun onPrepareActionMode(
|
|
587
|
+
mode: ActionMode,
|
|
588
|
+
menu: Menu,
|
|
589
|
+
) = callback?.onPrepareActionMode(mode, menu) ?: false
|
|
590
|
+
|
|
591
|
+
override fun onActionItemClicked(
|
|
592
|
+
mode: ActionMode,
|
|
593
|
+
menuItem: MenuItem,
|
|
594
|
+
): Boolean {
|
|
595
|
+
val itemId = menuItem.itemId
|
|
596
|
+
if (itemId < CONTEXT_MENU_ITEM_ID) {
|
|
597
|
+
return callback?.onActionItemClicked(mode, menuItem) ?: false
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
val selStart = selection?.start ?: 0
|
|
601
|
+
val selEnd = selection?.end ?: 0
|
|
602
|
+
val itemText = contextMenuItems.getOrNull(itemId - CONTEXT_MENU_ITEM_ID)?.second ?: return false
|
|
603
|
+
emitContextMenuItemPressEvent(itemText)
|
|
604
|
+
mode.finish()
|
|
605
|
+
post {
|
|
606
|
+
// Ensures selection is not lost after the action mode is finished
|
|
607
|
+
if (selStart in 0..selEnd) {
|
|
608
|
+
setSelection(selStart, selEnd)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return true
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
override fun onDestroyActionMode(mode: ActionMode) {
|
|
615
|
+
callback?.onDestroyActionMode(mode)
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return super.startActionMode(wrappedCallback, type)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private fun emitContextMenuItemPressEvent(itemText: String) {
|
|
623
|
+
val start = selection?.start ?: return
|
|
624
|
+
val end = selection.end
|
|
625
|
+
val styleState = spanState?.getStyleStatePayload() ?: return
|
|
626
|
+
val selectedText = text?.subSequence(start, end)?.toString() ?: ""
|
|
627
|
+
|
|
628
|
+
val reactContext = context as ReactContext
|
|
629
|
+
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
|
|
630
|
+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
|
|
631
|
+
dispatcher?.dispatchEvent(
|
|
632
|
+
OnContextMenuItemPressEvent(surfaceId, id, itemText, selectedText, start, end, styleState, experimentalSynchronousEvents),
|
|
633
|
+
)
|
|
634
|
+
}
|
|
635
|
+
|
|
499
636
|
// https://github.com/facebook/react-native/blob/36df97f500aa0aa8031098caf7526db358b6ddc1/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt#L283C2-L284C1
|
|
500
637
|
// After the text changes inside an EditText, TextView checks if a layout() has been requested.
|
|
501
638
|
// If it has, it will not scroll the text to the end of the new text inserted, but wait for the
|
|
@@ -550,7 +687,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
550
687
|
EnrichedSpans.ORDERED_LIST -> listStyles?.toggleStyle(EnrichedSpans.ORDERED_LIST)
|
|
551
688
|
EnrichedSpans.UNORDERED_LIST -> listStyles?.toggleStyle(EnrichedSpans.UNORDERED_LIST)
|
|
552
689
|
EnrichedSpans.CHECKBOX_LIST -> listStyles?.toggleStyle(EnrichedSpans.CHECKBOX_LIST)
|
|
553
|
-
else -> Log.w(
|
|
690
|
+
else -> Log.w(TAG, "Unknown style: $name")
|
|
554
691
|
}
|
|
555
692
|
|
|
556
693
|
layoutManager.invalidateLayout()
|
|
@@ -685,6 +822,13 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
685
822
|
parametrizedStyles?.setLinkSpan(start, end, text, url)
|
|
686
823
|
}
|
|
687
824
|
|
|
825
|
+
fun removeLink(
|
|
826
|
+
start: Int,
|
|
827
|
+
end: Int,
|
|
828
|
+
) {
|
|
829
|
+
parametrizedStyles?.removeLinkSpans(start, end)
|
|
830
|
+
}
|
|
831
|
+
|
|
688
832
|
fun addImage(
|
|
689
833
|
src: String,
|
|
690
834
|
width: Float,
|
|
@@ -719,7 +863,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
719
863
|
val html =
|
|
720
864
|
try {
|
|
721
865
|
EnrichedParser.toHtmlWithDefault(text)
|
|
722
|
-
} catch (
|
|
866
|
+
} catch (_: Exception) {
|
|
723
867
|
null
|
|
724
868
|
}
|
|
725
869
|
|
|
@@ -846,6 +990,8 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
846
990
|
}
|
|
847
991
|
|
|
848
992
|
companion object {
|
|
993
|
+
const val TAG = "EnrichedTextInputView"
|
|
849
994
|
const val CLIPBOARD_TAG = "react-native-enriched-clipboard"
|
|
995
|
+
private const val CONTEXT_MENU_ITEM_ID = 10000
|
|
850
996
|
}
|
|
851
997
|
}
|
package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt
CHANGED
|
@@ -19,6 +19,7 @@ import com.swmansion.enriched.textinput.events.OnChangeHtmlEvent
|
|
|
19
19
|
import com.swmansion.enriched.textinput.events.OnChangeSelectionEvent
|
|
20
20
|
import com.swmansion.enriched.textinput.events.OnChangeStateEvent
|
|
21
21
|
import com.swmansion.enriched.textinput.events.OnChangeTextEvent
|
|
22
|
+
import com.swmansion.enriched.textinput.events.OnContextMenuItemPressEvent
|
|
22
23
|
import com.swmansion.enriched.textinput.events.OnInputBlurEvent
|
|
23
24
|
import com.swmansion.enriched.textinput.events.OnInputFocusEvent
|
|
24
25
|
import com.swmansion.enriched.textinput.events.OnInputKeyPressEvent
|
|
@@ -72,6 +73,7 @@ class EnrichedTextInputViewManager :
|
|
|
72
73
|
map.put(OnRequestHtmlResultEvent.EVENT_NAME, mapOf("registrationName" to OnRequestHtmlResultEvent.EVENT_NAME))
|
|
73
74
|
map.put(OnInputKeyPressEvent.EVENT_NAME, mapOf("registrationName" to OnInputKeyPressEvent.EVENT_NAME))
|
|
74
75
|
map.put(OnPasteImagesEvent.EVENT_NAME, mapOf("registrationName" to OnPasteImagesEvent.EVENT_NAME))
|
|
76
|
+
map.put(OnContextMenuItemPressEvent.EVENT_NAME, mapOf("registrationName" to OnContextMenuItemPressEvent.EVENT_NAME))
|
|
75
77
|
|
|
76
78
|
return map
|
|
77
79
|
}
|
|
@@ -173,6 +175,14 @@ class EnrichedTextInputViewManager :
|
|
|
173
175
|
view?.setFontSize(size)
|
|
174
176
|
}
|
|
175
177
|
|
|
178
|
+
@ReactProp(name = "lineHeight", defaultFloat = 0f)
|
|
179
|
+
override fun setLineHeight(
|
|
180
|
+
view: EnrichedTextInputView?,
|
|
181
|
+
height: Float,
|
|
182
|
+
) {
|
|
183
|
+
view?.setLineHeight(height)
|
|
184
|
+
}
|
|
185
|
+
|
|
176
186
|
@ReactProp(name = "fontFamily")
|
|
177
187
|
override fun setFontFamily(
|
|
178
188
|
view: EnrichedTextInputView?,
|
|
@@ -257,6 +267,20 @@ class EnrichedTextInputViewManager :
|
|
|
257
267
|
view?.experimentalSynchronousEvents = value
|
|
258
268
|
}
|
|
259
269
|
|
|
270
|
+
override fun setContextMenuItems(
|
|
271
|
+
view: EnrichedTextInputView?,
|
|
272
|
+
value: ReadableArray?,
|
|
273
|
+
) {
|
|
274
|
+
view?.setContextMenuItems(value)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
override fun setUseHtmlNormalizer(
|
|
278
|
+
view: EnrichedTextInputView?,
|
|
279
|
+
value: Boolean,
|
|
280
|
+
) {
|
|
281
|
+
view?.useHtmlNormalizer = value
|
|
282
|
+
}
|
|
283
|
+
|
|
260
284
|
override fun focus(view: EnrichedTextInputView?) {
|
|
261
285
|
view?.requestFocusProgrammatically()
|
|
262
286
|
}
|
|
@@ -357,6 +381,14 @@ class EnrichedTextInputViewManager :
|
|
|
357
381
|
view?.addLink(start, end, text, url)
|
|
358
382
|
}
|
|
359
383
|
|
|
384
|
+
override fun removeLink(
|
|
385
|
+
view: EnrichedTextInputView?,
|
|
386
|
+
start: Int,
|
|
387
|
+
end: Int,
|
|
388
|
+
) {
|
|
389
|
+
view?.removeLink(start, end)
|
|
390
|
+
}
|
|
391
|
+
|
|
360
392
|
override fun addImage(
|
|
361
393
|
view: EnrichedTextInputView?,
|
|
362
394
|
src: String,
|
|
@@ -5,6 +5,7 @@ import android.graphics.Typeface
|
|
|
5
5
|
import android.graphics.text.LineBreaker
|
|
6
6
|
import android.os.Build
|
|
7
7
|
import android.text.Spannable
|
|
8
|
+
import android.text.SpannableString
|
|
8
9
|
import android.text.StaticLayout
|
|
9
10
|
import android.text.TextPaint
|
|
10
11
|
import android.util.Log
|
|
@@ -16,6 +17,7 @@ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
|
|
|
16
17
|
import com.facebook.yoga.YogaMeasureMode
|
|
17
18
|
import com.facebook.yoga.YogaMeasureOutput
|
|
18
19
|
import com.swmansion.enriched.common.parser.EnrichedParser
|
|
20
|
+
import com.swmansion.enriched.textinput.spans.EnrichedLineHeightSpan
|
|
19
21
|
import com.swmansion.enriched.textinput.styles.HtmlStyle
|
|
20
22
|
import java.util.concurrent.ConcurrentHashMap
|
|
21
23
|
import kotlin.math.ceil
|
|
@@ -141,16 +143,31 @@ object MeasurementStore {
|
|
|
141
143
|
): Long {
|
|
142
144
|
val defaultView = EnrichedTextInputView(context)
|
|
143
145
|
|
|
144
|
-
val
|
|
146
|
+
val rawText = getInitialText(defaultView, props)
|
|
145
147
|
val fontSize = getInitialFontSize(defaultView, props)
|
|
148
|
+
val lineHeight = props?.getDouble("lineHeight")?.toFloat() ?: 0f
|
|
146
149
|
|
|
147
150
|
val fontFamily = props?.getString("fontFamily")
|
|
148
151
|
val fontStyle = parseFontStyle(props?.getString("fontStyle"))
|
|
149
152
|
val fontWeight = parseFontWeight(props?.getString("fontWeight"))
|
|
150
153
|
|
|
154
|
+
val text: CharSequence =
|
|
155
|
+
if (lineHeight > 0f) {
|
|
156
|
+
val spannable = SpannableString(rawText)
|
|
157
|
+
spannable.setSpan(
|
|
158
|
+
EnrichedLineHeightSpan(lineHeight),
|
|
159
|
+
0,
|
|
160
|
+
spannable.length,
|
|
161
|
+
Spannable.SPAN_INCLUSIVE_INCLUSIVE,
|
|
162
|
+
)
|
|
163
|
+
spannable
|
|
164
|
+
} else {
|
|
165
|
+
rawText
|
|
166
|
+
}
|
|
167
|
+
|
|
151
168
|
val typeface = applyStyles(defaultView.typeface, fontStyle, fontWeight, fontFamily, context.assets)
|
|
152
169
|
val paintParams = PaintParams(typeface, fontSize)
|
|
153
|
-
val size = measure(width, text,
|
|
170
|
+
val size = measure(width, text, paintParams)
|
|
154
171
|
|
|
155
172
|
if (id != null) {
|
|
156
173
|
data[id] = MeasurementParams(true, width, size, text, paintParams)
|
package/android/src/main/java/com/swmansion/enriched/textinput/events/OnContextMenuItemPressEvent.kt
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package com.swmansion.enriched.textinput.events
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import com.facebook.react.bridge.Arguments
|
|
5
|
+
import com.facebook.react.bridge.WritableMap
|
|
6
|
+
import com.facebook.react.uimanager.events.Event
|
|
7
|
+
|
|
8
|
+
class OnContextMenuItemPressEvent(
|
|
9
|
+
surfaceId: Int,
|
|
10
|
+
viewId: Int,
|
|
11
|
+
private val itemText: String,
|
|
12
|
+
private val selectedText: String,
|
|
13
|
+
private val selectionStart: Int,
|
|
14
|
+
private val selectionEnd: Int,
|
|
15
|
+
private val styleState: WritableMap,
|
|
16
|
+
private val experimentalSynchronousEvents: Boolean,
|
|
17
|
+
) : Event<OnContextMenuItemPressEvent>(surfaceId, viewId) {
|
|
18
|
+
override fun getEventName(): String = EVENT_NAME
|
|
19
|
+
|
|
20
|
+
override fun getEventData(): WritableMap {
|
|
21
|
+
val eventData: WritableMap = Arguments.createMap()
|
|
22
|
+
eventData.putString("itemText", itemText)
|
|
23
|
+
eventData.putString("selectedText", selectedText)
|
|
24
|
+
eventData.putInt("selectionStart", selectionStart)
|
|
25
|
+
eventData.putInt("selectionEnd", selectionEnd)
|
|
26
|
+
eventData.putMap("styleState", styleState)
|
|
27
|
+
return eventData
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override fun experimental_isSynchronous(): Boolean = experimentalSynchronousEvents
|
|
31
|
+
|
|
32
|
+
companion object {
|
|
33
|
+
const val EVENT_NAME: String = "onContextMenuItemPress"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedLineHeightSpan.kt
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
package com.swmansion.enriched.textinput.spans
|
|
2
|
+
|
|
3
|
+
import android.graphics.Paint
|
|
4
|
+
import android.text.Spannable
|
|
5
|
+
import android.text.TextPaint
|
|
6
|
+
import android.text.style.LineHeightSpan
|
|
7
|
+
import android.text.style.MetricAffectingSpan
|
|
8
|
+
import com.facebook.react.uimanager.PixelUtil
|
|
9
|
+
import com.swmansion.enriched.common.spans.interfaces.EnrichedHeadingSpan
|
|
10
|
+
|
|
11
|
+
class EnrichedLineHeightSpan(
|
|
12
|
+
val lineHeight: Float,
|
|
13
|
+
) : MetricAffectingSpan(),
|
|
14
|
+
LineHeightSpan {
|
|
15
|
+
override fun updateDrawState(p0: TextPaint?) {
|
|
16
|
+
// Do nothing but inform TextView that line height should be recalculated
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun updateMeasureState(p0: TextPaint) {
|
|
20
|
+
// Do nothing but inform TextView that line height should be recalculated
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
override fun chooseHeight(
|
|
24
|
+
text: CharSequence,
|
|
25
|
+
start: Int,
|
|
26
|
+
end: Int,
|
|
27
|
+
spanstartv: Int,
|
|
28
|
+
v: Int,
|
|
29
|
+
fm: Paint.FontMetricsInt,
|
|
30
|
+
) {
|
|
31
|
+
val spannable = text as? Spannable ?: return
|
|
32
|
+
// Do not modify line height for headings
|
|
33
|
+
// In the future we may consider adding custom lineHeight support for each paragraph style
|
|
34
|
+
if (spannable.getSpans(start, end, EnrichedHeadingSpan::class.java).isNotEmpty()) return
|
|
35
|
+
|
|
36
|
+
val lineHeightPx = PixelUtil.toPixelFromSP(lineHeight)
|
|
37
|
+
val currentHeight = (fm.descent - fm.ascent).toFloat()
|
|
38
|
+
if (lineHeightPx <= currentHeight) return
|
|
39
|
+
|
|
40
|
+
val extra = (lineHeightPx - currentHeight).toInt()
|
|
41
|
+
fm.ascent -= extra
|
|
42
|
+
fm.top = minOf(fm.top, fm.ascent)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -108,6 +108,9 @@ class ParagraphStyles(
|
|
|
108
108
|
spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// Removes spans of the given type in the specified range.
|
|
112
|
+
// If the removed span intersects with the range, it will be split and the remaining part will be re-applied after the removal
|
|
113
|
+
// Returns true if any spans were removed, false otherwise
|
|
111
114
|
private fun <T> removeSpansForRange(
|
|
112
115
|
spannable: Spannable,
|
|
113
116
|
start: Int,
|
|
@@ -115,21 +118,24 @@ class ParagraphStyles(
|
|
|
115
118
|
clazz: Class<T>,
|
|
116
119
|
): Boolean {
|
|
117
120
|
val ssb = spannable as SpannableStringBuilder
|
|
118
|
-
var finalStart = start
|
|
119
|
-
var finalEnd = end
|
|
120
|
-
|
|
121
121
|
val spans = ssb.getSpans(start, end, clazz)
|
|
122
122
|
if (spans.isEmpty()) return false
|
|
123
123
|
|
|
124
124
|
for (span in spans) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
val spanStart = ssb.getSpanStart(span)
|
|
126
|
+
val spanEnd = ssb.getSpanEnd(span)
|
|
128
127
|
ssb.removeSpan(span)
|
|
129
|
-
}
|
|
130
128
|
|
|
131
|
-
|
|
129
|
+
if (spanStart < start) {
|
|
130
|
+
setSpan(ssb, clazz, spanStart, start - 1)
|
|
131
|
+
}
|
|
132
132
|
|
|
133
|
+
if (spanEnd > end) {
|
|
134
|
+
setSpan(ssb, clazz, end + 1, spanEnd)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
ssb.removeZWS(start, end)
|
|
133
139
|
return true
|
|
134
140
|
}
|
|
135
141
|
|
|
@@ -215,11 +221,7 @@ class ParagraphStyles(
|
|
|
215
221
|
}
|
|
216
222
|
|
|
217
223
|
val currSpan = currParagraphSpans[0]
|
|
218
|
-
val nextSpan = getNextParagraphSpan(s, end, type)
|
|
219
|
-
|
|
220
|
-
if (nextSpan == null) {
|
|
221
|
-
return
|
|
222
|
-
}
|
|
224
|
+
val nextSpan = getNextParagraphSpan(s, end, type) ?: return
|
|
223
225
|
|
|
224
226
|
val newStart = s.getSpanStart(currSpan)
|
|
225
227
|
val newEnd = s.getSpanEnd(nextSpan)
|
|
@@ -341,9 +343,12 @@ class ParagraphStyles(
|
|
|
341
343
|
continue
|
|
342
344
|
}
|
|
343
345
|
|
|
346
|
+
// If removing text at the beginning of the line, we want to remove the span for the whole paragraph
|
|
344
347
|
if (isBackspace) {
|
|
345
|
-
|
|
348
|
+
val currentParagraphBounds = s.getParagraphBounds(endCursorPosition)
|
|
349
|
+
removeSpansForRange(s, currentParagraphBounds.first, currentParagraphBounds.second, config.clazz)
|
|
346
350
|
spanState.setStart(style, null)
|
|
351
|
+
continue
|
|
347
352
|
} else {
|
|
348
353
|
s.insert(endCursorPosition, EnrichedConstants.ZWS_STRING)
|
|
349
354
|
endCursorPosition += 1
|
|
@@ -69,6 +69,22 @@ class ParametrizedStyles(
|
|
|
69
69
|
isSettingLinkSpan = false
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
fun removeLinkSpans(
|
|
73
|
+
start: Int,
|
|
74
|
+
end: Int,
|
|
75
|
+
) {
|
|
76
|
+
val spannable = view.text as SpannableStringBuilder
|
|
77
|
+
val textLength = spannable.length
|
|
78
|
+
val clampedStart = minOf(start, end).coerceIn(0, textLength)
|
|
79
|
+
val clampedEnd = maxOf(start, end).coerceIn(0, textLength)
|
|
80
|
+
|
|
81
|
+
val spans = spannable.getSpans(clampedStart, clampedEnd, EnrichedInputLinkSpan::class.java)
|
|
82
|
+
for (span in spans) {
|
|
83
|
+
spannable.removeSpan(span)
|
|
84
|
+
}
|
|
85
|
+
view.selection?.validateStyles()
|
|
86
|
+
}
|
|
87
|
+
|
|
72
88
|
fun afterTextChanged(
|
|
73
89
|
s: Editable,
|
|
74
90
|
startCursorPosition: Int,
|
|
@@ -203,18 +203,7 @@ class EnrichedSpanState(
|
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
|
|
207
|
-
val context = view.context as ReactContext
|
|
208
|
-
val surfaceId = UIManagerHelper.getSurfaceId(context)
|
|
209
|
-
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
|
|
210
|
-
|
|
211
|
-
dispatchPayload(dispatcher, surfaceId)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
private fun dispatchPayload(
|
|
215
|
-
dispatcher: EventDispatcher?,
|
|
216
|
-
surfaceId: Int,
|
|
217
|
-
) {
|
|
206
|
+
fun getStyleStatePayload(): WritableMap {
|
|
218
207
|
val activeStyles =
|
|
219
208
|
listOfNotNull(
|
|
220
209
|
if (boldStart != null) EnrichedSpans.BOLD else null,
|
|
@@ -258,6 +247,23 @@ class EnrichedSpanState(
|
|
|
258
247
|
payload.putMap("mention", getStyleState(activeStyles, EnrichedSpans.MENTION))
|
|
259
248
|
payload.putMap("checkboxList", getStyleState(activeStyles, EnrichedSpans.CHECKBOX_LIST))
|
|
260
249
|
|
|
250
|
+
return payload
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private fun emitStateChangeEvent() {
|
|
254
|
+
val context = view.context as ReactContext
|
|
255
|
+
val surfaceId = UIManagerHelper.getSurfaceId(context)
|
|
256
|
+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
|
|
257
|
+
|
|
258
|
+
dispatchPayload(dispatcher, surfaceId)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private fun dispatchPayload(
|
|
262
|
+
dispatcher: EventDispatcher?,
|
|
263
|
+
surfaceId: Int,
|
|
264
|
+
) {
|
|
265
|
+
val payload = getStyleStatePayload()
|
|
266
|
+
|
|
261
267
|
// Do not emit event if payload is the same
|
|
262
268
|
if (previousPayload == payload) {
|
|
263
269
|
return
|
|
@@ -8,8 +8,13 @@ set(LIB_ANDROID_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../..)
|
|
|
8
8
|
set(LIB_ANDROID_GENERATED_JNI_DIR ${LIB_ANDROID_DIR}/generated/jni)
|
|
9
9
|
set(LIB_ANDROID_GENERATED_COMPONENTS_DIR ${LIB_ANDROID_GENERATED_JNI_DIR}/react/renderer/components/${LIB_LITERAL})
|
|
10
10
|
|
|
11
|
+
set(LIB_CPP_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../cpp)
|
|
12
|
+
|
|
11
13
|
file(GLOB LIB_MODULE_SRCS CONFIGURE_DEPENDS *.cpp react/renderer/components/${LIB_LITERAL}/*.cpp)
|
|
12
14
|
file(GLOB LIB_CODEGEN_SRCS CONFIGURE_DEPENDS ${LIB_ANDROID_GENERATED_COMPONENTS_DIR}/*.cpp)
|
|
15
|
+
file(GLOB LIB_CPP_SRCS CONFIGURE_DEPENDS ${LIB_CPP_DIR}/parser/GumboParser.cpp ${LIB_CPP_DIR}/parser/GumboNormalizer.c)
|
|
16
|
+
|
|
17
|
+
set_source_files_properties(${LIB_CPP_DIR}/parser/GumboNormalizer.c PROPERTIES LANGUAGE C COMPILE_FLAGS "-std=c99")
|
|
13
18
|
|
|
14
19
|
if(NOT DEFINED REACT_NATIVE_MINOR_VERSION)
|
|
15
20
|
set(REACT_NATIVE_MINOR_VERSION ${ReactAndroid_VERSION_MINOR})
|
|
@@ -23,6 +28,7 @@ add_library(
|
|
|
23
28
|
${LIB_MODULE_SRCS}
|
|
24
29
|
${LIB_CUSTOM_SRCS}
|
|
25
30
|
${LIB_CODEGEN_SRCS}
|
|
31
|
+
${LIB_CPP_SRCS}
|
|
26
32
|
)
|
|
27
33
|
|
|
28
34
|
target_include_directories(
|
|
@@ -33,6 +39,8 @@ target_include_directories(
|
|
|
33
39
|
${LIB_ANDROID_GENERATED_JNI_DIR}
|
|
34
40
|
${LIB_ANDROID_GENERATED_COMPONENTS_DIR}
|
|
35
41
|
${LIB_CPP_DIR}
|
|
42
|
+
${LIB_CPP_DIR}/parser
|
|
43
|
+
${LIB_CPP_DIR}/GumboParser
|
|
36
44
|
)
|
|
37
45
|
|
|
38
46
|
find_package(fbjni REQUIRED CONFIG)
|
|
@@ -45,19 +53,7 @@ target_link_libraries(
|
|
|
45
53
|
ReactAndroid::reactnative
|
|
46
54
|
)
|
|
47
55
|
|
|
48
|
-
|
|
49
|
-
target_compile_reactnative_options(${LIB_TARGET_NAME} PRIVATE)
|
|
50
|
-
else()
|
|
51
|
-
target_compile_options(
|
|
52
|
-
${LIB_TARGET_NAME}
|
|
53
|
-
PRIVATE
|
|
54
|
-
-DLOG_TAG=\"ReactNative\"
|
|
55
|
-
-fexceptions
|
|
56
|
-
-frtti
|
|
57
|
-
-Wall
|
|
58
|
-
-std=c++20
|
|
59
|
-
)
|
|
60
|
-
endif()
|
|
56
|
+
target_compile_reactnative_options(${LIB_TARGET_NAME} PRIVATE)
|
|
61
57
|
|
|
62
58
|
target_include_directories(
|
|
63
59
|
${CMAKE_PROJECT_NAME}
|