react-native-enriched 0.0.0 → 0.1.1
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/LICENSE +20 -0
- package/README.md +875 -0
- package/ReactNativeEnriched.podspec +27 -0
- package/android/build.gradle +101 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +146 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +55 -0
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/ComponentDescriptors.cpp +22 -0
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/ComponentDescriptors.h +24 -0
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/EventEmitters.cpp +118 -0
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/EventEmitters.h +95 -0
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.cpp +128 -0
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +577 -0
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/ShadowNodes.cpp +17 -0
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/ShadowNodes.h +23 -0
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/States.cpp +16 -0
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/States.h +20 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +535 -0
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewLayoutManager.kt +64 -0
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +292 -0
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewPackage.kt +19 -0
- package/android/src/main/java/com/swmansion/enriched/events/MentionHandler.kt +40 -0
- package/android/src/main/java/com/swmansion/enriched/events/OnChangeHtmlEvent.kt +28 -0
- package/android/src/main/java/com/swmansion/enriched/events/OnChangeSelectionEvent.kt +29 -0
- package/android/src/main/java/com/swmansion/enriched/events/OnChangeStateEvent.kt +24 -0
- package/android/src/main/java/com/swmansion/enriched/events/OnChangeTextEvent.kt +30 -0
- package/android/src/main/java/com/swmansion/enriched/events/OnInputBlurEvent.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/events/OnInputFocusEvent.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/events/OnLinkDetectedEvent.kt +30 -0
- package/android/src/main/java/com/swmansion/enriched/events/OnMentionDetectedEvent.kt +29 -0
- package/android/src/main/java/com/swmansion/enriched/events/OnMentionEvent.kt +33 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedBlockQuoteSpan.kt +34 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedBoldSpan.kt +10 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt +38 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH1Span.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH2Span.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH3Span.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedImageSpan.kt +41 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedInlineCodeSpan.kt +16 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedItalicSpan.kt +10 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedLinkSpan.kt +24 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedMentionSpan.kt +36 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedOrderedListSpan.kt +71 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +111 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedStrikeThroughSpan.kt +9 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnderlineSpan.kt +9 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnorderedListSpan.kt +49 -0
- package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedBlockSpan.kt +4 -0
- package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedHeadingSpan.kt +4 -0
- package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedInlineSpan.kt +4 -0
- package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedParagraphSpan.kt +4 -0
- package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedSpan.kt +4 -0
- package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedZeroWidthSpaceSpan.kt +5 -0
- package/android/src/main/java/com/swmansion/enriched/styles/HtmlStyle.kt +227 -0
- package/android/src/main/java/com/swmansion/enriched/styles/InlineStyles.kt +146 -0
- package/android/src/main/java/com/swmansion/enriched/styles/ListStyles.kt +173 -0
- package/android/src/main/java/com/swmansion/enriched/styles/ParagraphStyles.kt +186 -0
- package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +223 -0
- package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +857 -0
- package/android/src/main/java/com/swmansion/enriched/utils/EnrichedSelection.kt +285 -0
- package/android/src/main/java/com/swmansion/enriched/utils/EnrichedSpanState.kt +204 -0
- package/android/src/main/java/com/swmansion/enriched/utils/Utils.kt +91 -0
- package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedSpanWatcher.kt +73 -0
- package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt +51 -0
- package/android/src/main/new_arch/CMakeLists.txt +56 -0
- package/android/src/main/new_arch/RNEnrichedTextInputViewSpec.cpp +22 -0
- package/android/src/main/new_arch/RNEnrichedTextInputViewSpec.h +26 -0
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputComponentDescriptor.h +35 -0
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp +51 -0
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h +26 -0
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp +34 -0
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.h +54 -0
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputState.cpp +9 -0
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputState.h +25 -0
- package/ios/EnrichedTextInputView.h +33 -0
- package/ios/EnrichedTextInputView.mm +1190 -0
- package/ios/EnrichedTextInputViewManager.mm +13 -0
- package/ios/config/InputConfig.h +67 -0
- package/ios/config/InputConfig.mm +382 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/ComponentDescriptors.cpp +22 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/ComponentDescriptors.h +24 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/EventEmitters.cpp +118 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/EventEmitters.h +95 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/Props.cpp +128 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +577 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/RCTComponentViewHelpers.h +384 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/ShadowNodes.cpp +17 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/ShadowNodes.h +23 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/States.cpp +16 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/States.h +20 -0
- package/ios/inputParser/InputParser.h +11 -0
- package/ios/inputParser/InputParser.mm +659 -0
- package/ios/inputTextView/InputTextView.h +6 -0
- package/ios/inputTextView/InputTextView.mm +115 -0
- package/ios/internals/EnrichedTextInputViewComponentDescriptor.h +17 -0
- package/ios/internals/EnrichedTextInputViewShadowNode.h +40 -0
- package/ios/internals/EnrichedTextInputViewShadowNode.mm +83 -0
- package/ios/internals/EnrichedTextInputViewState.cpp +10 -0
- package/ios/internals/EnrichedTextInputViewState.h +20 -0
- package/ios/styles/BlockQuoteStyle.mm +248 -0
- package/ios/styles/BoldStyle.mm +122 -0
- package/ios/styles/H1Style.mm +10 -0
- package/ios/styles/H2Style.mm +10 -0
- package/ios/styles/H3Style.mm +10 -0
- package/ios/styles/HeadingStyleBase.mm +144 -0
- package/ios/styles/InlineCodeStyle.mm +163 -0
- package/ios/styles/ItalicStyle.mm +110 -0
- package/ios/styles/LinkStyle.mm +463 -0
- package/ios/styles/MentionStyle.mm +476 -0
- package/ios/styles/OrderedListStyle.mm +225 -0
- package/ios/styles/StrikethroughStyle.mm +80 -0
- package/ios/styles/UnderlineStyle.mm +112 -0
- package/ios/styles/UnorderedListStyle.mm +225 -0
- package/ios/utils/BaseStyleProtocol.h +16 -0
- package/ios/utils/ColorExtension.h +6 -0
- package/ios/utils/ColorExtension.mm +27 -0
- package/ios/utils/FontExtension.h +13 -0
- package/ios/utils/FontExtension.mm +91 -0
- package/ios/utils/LayoutManagerExtension.h +6 -0
- package/ios/utils/LayoutManagerExtension.mm +171 -0
- package/ios/utils/LinkData.h +9 -0
- package/ios/utils/LinkData.mm +4 -0
- package/ios/utils/MentionParams.h +9 -0
- package/ios/utils/MentionParams.mm +4 -0
- package/ios/utils/MentionStyleProps.h +13 -0
- package/ios/utils/MentionStyleProps.mm +56 -0
- package/ios/utils/OccurenceUtils.h +37 -0
- package/ios/utils/OccurenceUtils.mm +124 -0
- package/ios/utils/ParagraphsUtils.h +7 -0
- package/ios/utils/ParagraphsUtils.mm +54 -0
- package/ios/utils/StringExtension.h +15 -0
- package/ios/utils/StringExtension.mm +57 -0
- package/ios/utils/StyleHeaders.h +74 -0
- package/ios/utils/StylePair.h +9 -0
- package/ios/utils/StylePair.mm +4 -0
- package/ios/utils/StyleTypeEnum.h +22 -0
- package/ios/utils/TextDecorationLineEnum.h +6 -0
- package/ios/utils/TextDecorationLineEnum.mm +4 -0
- package/ios/utils/TextInsertionUtils.h +6 -0
- package/ios/utils/TextInsertionUtils.mm +48 -0
- package/ios/utils/WordsUtils.h +6 -0
- package/ios/utils/WordsUtils.mm +88 -0
- package/ios/utils/ZeroWidthSpaceUtils.h +7 -0
- package/ios/utils/ZeroWidthSpaceUtils.mm +164 -0
- package/lib/module/EnrichedTextInput.js +191 -0
- package/lib/module/EnrichedTextInput.js.map +1 -0
- package/lib/module/EnrichedTextInputNativeComponent.ts +235 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/normalizeHtmlStyle.js +141 -0
- package/lib/module/normalizeHtmlStyle.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/EnrichedTextInput.d.ts +113 -0
- package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -0
- package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +160 -0
- package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/normalizeHtmlStyle.d.ts +4 -0
- package/lib/typescript/src/normalizeHtmlStyle.d.ts.map +1 -0
- package/package.json +172 -1
- package/react-native.config.js +13 -0
- package/src/EnrichedTextInput.tsx +358 -0
- package/src/EnrichedTextInputNativeComponent.ts +235 -0
- package/src/index.tsx +9 -0
- package/src/normalizeHtmlStyle.ts +188 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
package com.swmansion.enriched.utils
|
|
2
|
+
|
|
3
|
+
import android.text.Editable
|
|
4
|
+
import android.text.Spannable
|
|
5
|
+
import com.facebook.react.bridge.ReactContext
|
|
6
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
7
|
+
import com.swmansion.enriched.EnrichedTextInputView
|
|
8
|
+
import com.swmansion.enriched.events.OnChangeSelectionEvent
|
|
9
|
+
import com.swmansion.enriched.events.OnLinkDetectedEvent
|
|
10
|
+
import com.swmansion.enriched.events.OnMentionDetectedEvent
|
|
11
|
+
import com.swmansion.enriched.spans.EnrichedLinkSpan
|
|
12
|
+
import com.swmansion.enriched.spans.EnrichedMentionSpan
|
|
13
|
+
import com.swmansion.enriched.spans.EnrichedSpans
|
|
14
|
+
import org.json.JSONObject
|
|
15
|
+
|
|
16
|
+
class EnrichedSelection(private val view: EnrichedTextInputView) {
|
|
17
|
+
var start: Int = 0
|
|
18
|
+
var end: Int = 0
|
|
19
|
+
|
|
20
|
+
private var previousLinkDetectedEvent: MutableMap<String, String> = mutableMapOf("text" to "", "url" to "")
|
|
21
|
+
private var previousMentionDetectedEvent: MutableMap<String, String> = mutableMapOf("text" to "", "payload" to "")
|
|
22
|
+
|
|
23
|
+
fun onSelection(selStart: Int, selEnd: Int) {
|
|
24
|
+
var shouldValidateStyles = false
|
|
25
|
+
var newStart = start
|
|
26
|
+
var newEnd = end
|
|
27
|
+
|
|
28
|
+
if (selStart != -1 && selStart != newStart) {
|
|
29
|
+
newStart = selStart
|
|
30
|
+
shouldValidateStyles = true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (selEnd != -1 && selEnd != newEnd) {
|
|
34
|
+
newEnd = selEnd
|
|
35
|
+
shouldValidateStyles = true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
val textLength = view.text?.length ?: 0
|
|
39
|
+
val finalStart = newStart.coerceAtMost(newEnd).coerceAtLeast(0).coerceAtMost(textLength)
|
|
40
|
+
val finalEnd = newEnd.coerceAtLeast(newStart).coerceAtLeast(0).coerceAtMost(textLength)
|
|
41
|
+
|
|
42
|
+
if (isZeroWidthSelection(finalStart, finalEnd) && !view.isSettingValue) {
|
|
43
|
+
view.setSelection(finalStart + 1)
|
|
44
|
+
shouldValidateStyles = false
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!shouldValidateStyles) return
|
|
48
|
+
|
|
49
|
+
start = finalStart
|
|
50
|
+
end = finalEnd
|
|
51
|
+
validateStyles()
|
|
52
|
+
emitSelectionChangeEvent(view.text, finalStart, finalEnd)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private fun isZeroWidthSelection(start: Int, end: Int): Boolean {
|
|
56
|
+
val text = view.text ?: return false
|
|
57
|
+
|
|
58
|
+
if (start != end) {
|
|
59
|
+
return text.substring(start, end) == "\u200B"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
val isNewLine = if (start > 0 ) text.substring(start - 1, start) == "\n" else true
|
|
63
|
+
val isNextCharacterZeroWidth = if (start < text.length) {
|
|
64
|
+
text.substring(start, start + 1) == "\u200B"
|
|
65
|
+
} else {
|
|
66
|
+
false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return isNewLine && isNextCharacterZeroWidth
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fun validateStyles() {
|
|
73
|
+
val state = view.spanState ?: return
|
|
74
|
+
|
|
75
|
+
// We don't validate inline styles when removing many characters at once
|
|
76
|
+
// We don't want to remove styles on auto-correction
|
|
77
|
+
// If user removes many characters at once, we want to keep the styles config
|
|
78
|
+
if (!view.isRemovingMany) {
|
|
79
|
+
for ((style, config) in EnrichedSpans.inlineSpans) {
|
|
80
|
+
state.setStart(style, getInlineStyleStart(config.clazz))
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
view.isRemovingMany = false
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for ((style, config) in EnrichedSpans.paragraphSpans) {
|
|
87
|
+
state.setStart(style, getParagraphStyleStart(config.clazz))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for ((style, config) in EnrichedSpans.listSpans) {
|
|
91
|
+
state.setStart(style, getListStyleStart(config.clazz))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for ((style, config) in EnrichedSpans.parametrizedStyles) {
|
|
95
|
+
state.setStart(style, getParametrizedStyleStart(config.clazz))
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fun getInlineSelection(): Pair<Int, Int> {
|
|
100
|
+
val finalStart = start.coerceAtMost(end).coerceAtLeast(0)
|
|
101
|
+
val finalEnd = end.coerceAtLeast(start).coerceAtLeast(0)
|
|
102
|
+
|
|
103
|
+
return Pair(finalStart, finalEnd)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private fun <T>getInlineStyleStart(type: Class<T>): Int? {
|
|
107
|
+
val (start, end) = getInlineSelection()
|
|
108
|
+
val spannable = view.text as Spannable
|
|
109
|
+
val spans = spannable.getSpans(start, end, type)
|
|
110
|
+
var styleStart: Int? = null
|
|
111
|
+
|
|
112
|
+
for (span in spans) {
|
|
113
|
+
val spanStart = spannable.getSpanStart(span)
|
|
114
|
+
val spanEnd = spannable.getSpanEnd(span)
|
|
115
|
+
|
|
116
|
+
if (start == end && start == spanStart) {
|
|
117
|
+
styleStart = null
|
|
118
|
+
} else if (start >= spanStart && end <= spanEnd) {
|
|
119
|
+
styleStart = spanStart
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return styleStart
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fun getParagraphSelection(): Pair<Int, Int> {
|
|
127
|
+
val (currentStart, currentEnd) = getInlineSelection()
|
|
128
|
+
val spannable = view.text as Spannable
|
|
129
|
+
return spannable.getParagraphBounds(currentStart, currentEnd)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private fun <T>getParagraphStyleStart(type: Class<T>): Int? {
|
|
133
|
+
val (start, end) = getParagraphSelection()
|
|
134
|
+
val spannable = view.text as Spannable
|
|
135
|
+
val spans = spannable.getSpans(start, end, type)
|
|
136
|
+
var styleStart: Int? = null
|
|
137
|
+
|
|
138
|
+
for (span in spans) {
|
|
139
|
+
val spanStart = spannable.getSpanStart(span)
|
|
140
|
+
val spanEnd = spannable.getSpanEnd(span)
|
|
141
|
+
|
|
142
|
+
if (start >= spanStart && end <= spanEnd) {
|
|
143
|
+
styleStart = spanStart
|
|
144
|
+
break
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return styleStart
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private fun <T>getListStyleStart(type: Class<T>): Int? {
|
|
152
|
+
val (start, end) = getParagraphSelection()
|
|
153
|
+
val spannable = view.text as Spannable
|
|
154
|
+
var styleStart: Int? = null
|
|
155
|
+
|
|
156
|
+
var paragraphStart = start
|
|
157
|
+
val paragraphs = spannable.substring(start, end).split("\n")
|
|
158
|
+
pi@ for (paragraph in paragraphs) {
|
|
159
|
+
val paragraphEnd = paragraphStart + paragraph.length
|
|
160
|
+
val spans = spannable.getSpans(paragraphStart, paragraphEnd, type)
|
|
161
|
+
|
|
162
|
+
for (span in spans) {
|
|
163
|
+
val spanStart = spannable.getSpanStart(span)
|
|
164
|
+
val spanEnd = spannable.getSpanEnd(span)
|
|
165
|
+
|
|
166
|
+
if (spanStart == paragraphStart && spanEnd == paragraphEnd) {
|
|
167
|
+
styleStart = spanStart
|
|
168
|
+
paragraphStart = paragraphEnd + 1
|
|
169
|
+
continue@pi
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
styleStart = null
|
|
174
|
+
break
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return styleStart
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private fun <T>getParametrizedStyleStart(type: Class<T>): Int? {
|
|
181
|
+
val (start, end) = getInlineSelection()
|
|
182
|
+
val spannable = view.text as Spannable
|
|
183
|
+
val spans = spannable.getSpans(start, end, type)
|
|
184
|
+
val isLinkType = type == EnrichedLinkSpan::class.java
|
|
185
|
+
val isMentionType = type == EnrichedMentionSpan::class.java
|
|
186
|
+
|
|
187
|
+
if (isLinkType && spans.isEmpty()) {
|
|
188
|
+
emitLinkDetectedEvent(spannable, null, start, end)
|
|
189
|
+
return null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (isMentionType && spans.isEmpty()) {
|
|
193
|
+
emitMentionDetectedEvent(spannable, null, start, end)
|
|
194
|
+
return null
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (span in spans) {
|
|
198
|
+
val spanStart = spannable.getSpanStart(span)
|
|
199
|
+
val spanEnd = spannable.getSpanEnd(span)
|
|
200
|
+
|
|
201
|
+
if (start >= spanStart && end <= spanEnd) {
|
|
202
|
+
if (isLinkType && span is EnrichedLinkSpan) {
|
|
203
|
+
emitLinkDetectedEvent(spannable, span, spanStart, spanEnd)
|
|
204
|
+
} else if (isMentionType && span is EnrichedMentionSpan) {
|
|
205
|
+
emitMentionDetectedEvent(spannable, span, spanStart, spanEnd)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return spanStart
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return null
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private fun emitSelectionChangeEvent(editable: Editable?, start: Int, end: Int) {
|
|
216
|
+
if (editable == null) return
|
|
217
|
+
|
|
218
|
+
val context = view.context as ReactContext
|
|
219
|
+
val surfaceId = UIManagerHelper.getSurfaceId(context)
|
|
220
|
+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
|
|
221
|
+
|
|
222
|
+
val text = editable.substring(start, end)
|
|
223
|
+
dispatcher?.dispatchEvent(OnChangeSelectionEvent(
|
|
224
|
+
surfaceId,
|
|
225
|
+
view.id,
|
|
226
|
+
text,
|
|
227
|
+
start ,
|
|
228
|
+
end,
|
|
229
|
+
view.experimentalSynchronousEvents,
|
|
230
|
+
))
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private fun emitLinkDetectedEvent(spannable: Spannable, span: EnrichedLinkSpan?, start: Int, end: Int) {
|
|
234
|
+
val text = spannable.substring(start, end)
|
|
235
|
+
val url = span?.getUrl() ?: ""
|
|
236
|
+
|
|
237
|
+
// Prevents emitting unnecessary events
|
|
238
|
+
if (text == previousLinkDetectedEvent["text"] && url == previousLinkDetectedEvent["url"]) return
|
|
239
|
+
|
|
240
|
+
previousLinkDetectedEvent.put("text", text)
|
|
241
|
+
previousLinkDetectedEvent.put("url", url)
|
|
242
|
+
|
|
243
|
+
val context = view.context as ReactContext
|
|
244
|
+
val surfaceId = UIManagerHelper.getSurfaceId(context)
|
|
245
|
+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
|
|
246
|
+
dispatcher?.dispatchEvent(OnLinkDetectedEvent(
|
|
247
|
+
surfaceId,
|
|
248
|
+
view.id,
|
|
249
|
+
text,
|
|
250
|
+
url,
|
|
251
|
+
start,
|
|
252
|
+
end,
|
|
253
|
+
view.experimentalSynchronousEvents,
|
|
254
|
+
))
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private fun emitMentionDetectedEvent(spannable: Spannable, span: EnrichedMentionSpan?, start: Int, end: Int) {
|
|
258
|
+
val text = spannable.substring(start, end)
|
|
259
|
+
val attributes = span?.getAttributes() ?: emptyMap()
|
|
260
|
+
val indicator = span?.getIndicator() ?: ""
|
|
261
|
+
val payload = JSONObject(attributes).toString()
|
|
262
|
+
|
|
263
|
+
val previousText = previousMentionDetectedEvent["text"] ?: ""
|
|
264
|
+
val previousPayload = previousMentionDetectedEvent["payload"] ?: ""
|
|
265
|
+
val previousIndicator = previousMentionDetectedEvent["indicator"] ?: ""
|
|
266
|
+
|
|
267
|
+
if (text == previousText && payload == previousPayload && indicator == previousIndicator) return
|
|
268
|
+
|
|
269
|
+
previousMentionDetectedEvent.put("text", text)
|
|
270
|
+
previousMentionDetectedEvent.put("payload", payload)
|
|
271
|
+
previousMentionDetectedEvent.put("indicator", indicator)
|
|
272
|
+
|
|
273
|
+
val context = view.context as ReactContext
|
|
274
|
+
val surfaceId = UIManagerHelper.getSurfaceId(context)
|
|
275
|
+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
|
|
276
|
+
dispatcher?.dispatchEvent(OnMentionDetectedEvent(
|
|
277
|
+
surfaceId,
|
|
278
|
+
view.id,
|
|
279
|
+
text,
|
|
280
|
+
indicator,
|
|
281
|
+
payload,
|
|
282
|
+
view.experimentalSynchronousEvents,
|
|
283
|
+
))
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
package com.swmansion.enriched.utils
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.ReactContext
|
|
5
|
+
import com.facebook.react.bridge.WritableMap
|
|
6
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
7
|
+
import com.swmansion.enriched.EnrichedTextInputView
|
|
8
|
+
import com.swmansion.enriched.events.OnChangeStateEvent
|
|
9
|
+
import com.swmansion.enriched.spans.EnrichedSpans
|
|
10
|
+
|
|
11
|
+
class EnrichedSpanState(private val view: EnrichedTextInputView) {
|
|
12
|
+
private var previousPayload: WritableMap? = null
|
|
13
|
+
|
|
14
|
+
var boldStart: Int? = null
|
|
15
|
+
private set
|
|
16
|
+
var italicStart: Int? = null
|
|
17
|
+
private set
|
|
18
|
+
var underlineStart: Int? = null
|
|
19
|
+
private set
|
|
20
|
+
var strikethroughStart: Int? = null
|
|
21
|
+
private set
|
|
22
|
+
var inlineCodeStart: Int? = null
|
|
23
|
+
private set
|
|
24
|
+
var h1Start: Int? = null
|
|
25
|
+
private set
|
|
26
|
+
var h2Start: Int? = null
|
|
27
|
+
private set
|
|
28
|
+
var h3Start: Int? = null
|
|
29
|
+
private set
|
|
30
|
+
var codeBlockStart: Int? = null
|
|
31
|
+
private set
|
|
32
|
+
var blockQuoteStart: Int? = null
|
|
33
|
+
private set
|
|
34
|
+
var orderedListStart: Int? = null
|
|
35
|
+
private set
|
|
36
|
+
var unorderedListStart: Int? = null
|
|
37
|
+
private set
|
|
38
|
+
var linkStart: Int? = null
|
|
39
|
+
private set
|
|
40
|
+
var imageStart: Int? = null
|
|
41
|
+
private set
|
|
42
|
+
var mentionStart: Int? = null
|
|
43
|
+
private set
|
|
44
|
+
|
|
45
|
+
fun setBoldStart(start: Int?) {
|
|
46
|
+
this.boldStart = start
|
|
47
|
+
emitStateChangeEvent()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fun setItalicStart(start: Int?) {
|
|
51
|
+
this.italicStart = start
|
|
52
|
+
emitStateChangeEvent()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fun setUnderlineStart(start: Int?) {
|
|
56
|
+
this.underlineStart = start
|
|
57
|
+
emitStateChangeEvent()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fun setStrikethroughStart(start: Int?) {
|
|
61
|
+
this.strikethroughStart = start
|
|
62
|
+
emitStateChangeEvent()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fun setInlineCodeStart(start: Int?) {
|
|
66
|
+
this.inlineCodeStart = start
|
|
67
|
+
emitStateChangeEvent()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fun setH1Start(start: Int?) {
|
|
71
|
+
this.h1Start = start
|
|
72
|
+
emitStateChangeEvent()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fun setH2Start(start: Int?) {
|
|
76
|
+
this.h2Start = start
|
|
77
|
+
emitStateChangeEvent()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fun setH3Start(start: Int?) {
|
|
81
|
+
this.h3Start = start
|
|
82
|
+
emitStateChangeEvent()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fun setCodeBlockStart(start: Int?) {
|
|
86
|
+
this.codeBlockStart = start
|
|
87
|
+
emitStateChangeEvent()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fun setBlockQuoteStart(start: Int?) {
|
|
91
|
+
this.blockQuoteStart = start
|
|
92
|
+
emitStateChangeEvent()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fun setOrderedListStart(start: Int?) {
|
|
96
|
+
this.orderedListStart = start
|
|
97
|
+
emitStateChangeEvent()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fun setUnorderedListStart(start: Int?) {
|
|
101
|
+
this.unorderedListStart = start
|
|
102
|
+
emitStateChangeEvent()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fun setLinkStart(start: Int?) {
|
|
106
|
+
this.linkStart = start
|
|
107
|
+
emitStateChangeEvent()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fun setImageStart(start: Int?) {
|
|
111
|
+
this.imageStart = start
|
|
112
|
+
emitStateChangeEvent()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fun setMentionStart(start: Int?) {
|
|
116
|
+
this.mentionStart = start
|
|
117
|
+
emitStateChangeEvent()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fun getStart(name: String): Int? {
|
|
121
|
+
val start = when (name) {
|
|
122
|
+
EnrichedSpans.BOLD -> boldStart
|
|
123
|
+
EnrichedSpans.ITALIC -> italicStart
|
|
124
|
+
EnrichedSpans.UNDERLINE -> underlineStart
|
|
125
|
+
EnrichedSpans.STRIKETHROUGH -> strikethroughStart
|
|
126
|
+
EnrichedSpans.INLINE_CODE -> inlineCodeStart
|
|
127
|
+
EnrichedSpans.H1 -> h1Start
|
|
128
|
+
EnrichedSpans.H2 -> h2Start
|
|
129
|
+
EnrichedSpans.H3 -> h3Start
|
|
130
|
+
EnrichedSpans.CODE_BLOCK -> codeBlockStart
|
|
131
|
+
EnrichedSpans.BLOCK_QUOTE -> blockQuoteStart
|
|
132
|
+
EnrichedSpans.ORDERED_LIST -> orderedListStart
|
|
133
|
+
EnrichedSpans.UNORDERED_LIST -> unorderedListStart
|
|
134
|
+
EnrichedSpans.LINK -> linkStart
|
|
135
|
+
EnrichedSpans.IMAGE -> imageStart
|
|
136
|
+
EnrichedSpans.MENTION -> mentionStart
|
|
137
|
+
else -> null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return start
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fun setStart(name: String, start: Int?) {
|
|
144
|
+
when (name) {
|
|
145
|
+
EnrichedSpans.BOLD -> setBoldStart(start)
|
|
146
|
+
EnrichedSpans.ITALIC -> setItalicStart(start)
|
|
147
|
+
EnrichedSpans.UNDERLINE -> setUnderlineStart(start)
|
|
148
|
+
EnrichedSpans.STRIKETHROUGH -> setStrikethroughStart(start)
|
|
149
|
+
EnrichedSpans.INLINE_CODE -> setInlineCodeStart(start)
|
|
150
|
+
EnrichedSpans.H1 -> setH1Start(start)
|
|
151
|
+
EnrichedSpans.H2 -> setH2Start(start)
|
|
152
|
+
EnrichedSpans.H3 -> setH3Start(start)
|
|
153
|
+
EnrichedSpans.CODE_BLOCK -> setCodeBlockStart(start)
|
|
154
|
+
EnrichedSpans.BLOCK_QUOTE -> setBlockQuoteStart(start)
|
|
155
|
+
EnrichedSpans.ORDERED_LIST -> setOrderedListStart(start)
|
|
156
|
+
EnrichedSpans.UNORDERED_LIST -> setUnorderedListStart(start)
|
|
157
|
+
EnrichedSpans.LINK -> setLinkStart(start)
|
|
158
|
+
EnrichedSpans.IMAGE -> setImageStart(start)
|
|
159
|
+
EnrichedSpans.MENTION -> setMentionStart(start)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private fun emitStateChangeEvent() {
|
|
164
|
+
val payload: WritableMap = Arguments.createMap()
|
|
165
|
+
payload.putBoolean("isBold", boldStart != null)
|
|
166
|
+
payload.putBoolean("isItalic", italicStart != null)
|
|
167
|
+
payload.putBoolean("isUnderline", underlineStart != null)
|
|
168
|
+
payload.putBoolean("isStrikeThrough", strikethroughStart != null)
|
|
169
|
+
payload.putBoolean("isInlineCode", inlineCodeStart != null)
|
|
170
|
+
payload.putBoolean("isH1", h1Start != null)
|
|
171
|
+
payload.putBoolean("isH2", h2Start != null)
|
|
172
|
+
payload.putBoolean("isH3", h3Start != null)
|
|
173
|
+
payload.putBoolean("isCodeBlock", codeBlockStart != null)
|
|
174
|
+
payload.putBoolean("isBlockQuote", blockQuoteStart != null)
|
|
175
|
+
payload.putBoolean("isOrderedList", orderedListStart != null)
|
|
176
|
+
payload.putBoolean("isUnorderedList", unorderedListStart != null)
|
|
177
|
+
payload.putBoolean("isLink", linkStart != null)
|
|
178
|
+
payload.putBoolean("isImage", imageStart != null)
|
|
179
|
+
payload.putBoolean("isMention", mentionStart != null)
|
|
180
|
+
|
|
181
|
+
// Do not emit event if payload is the same
|
|
182
|
+
if (previousPayload == payload) {
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
previousPayload = Arguments.createMap().apply {
|
|
187
|
+
merge(payload)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
val context = view.context as ReactContext
|
|
191
|
+
val surfaceId = UIManagerHelper.getSurfaceId(context)
|
|
192
|
+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
|
|
193
|
+
dispatcher?.dispatchEvent(OnChangeStateEvent(
|
|
194
|
+
surfaceId,
|
|
195
|
+
view.id,
|
|
196
|
+
payload,
|
|
197
|
+
view.experimentalSynchronousEvents,
|
|
198
|
+
))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
companion object {
|
|
202
|
+
const val NAME = "ReactNativeEnrichedView"
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
package com.swmansion.enriched.utils
|
|
2
|
+
|
|
3
|
+
import android.text.Spannable
|
|
4
|
+
import android.text.SpannableString
|
|
5
|
+
import android.text.SpannableStringBuilder
|
|
6
|
+
import android.util.Log
|
|
7
|
+
import com.swmansion.enriched.spans.interfaces.EnrichedBlockSpan
|
|
8
|
+
import com.swmansion.enriched.spans.interfaces.EnrichedParagraphSpan
|
|
9
|
+
import org.json.JSONObject
|
|
10
|
+
|
|
11
|
+
fun jsonStringToStringMap(json: String): Map<String, String> {
|
|
12
|
+
val result = mutableMapOf<String, String>()
|
|
13
|
+
try {
|
|
14
|
+
val jsonObject = JSONObject(json)
|
|
15
|
+
for (key in jsonObject.keys()) {
|
|
16
|
+
val value = jsonObject.opt(key)
|
|
17
|
+
if (value is String) {
|
|
18
|
+
result[key] = value
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
} catch (e: Exception) {
|
|
22
|
+
Log.w("ReactNativeEnrichedView", "Failed to parse JSON string to Map: $json", e)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return result
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fun Spannable.getSafeSpanBoundaries(start: Int, end: Int): Pair<Int, Int> {
|
|
29
|
+
val safeStart = start.coerceAtMost(end).coerceAtLeast(0)
|
|
30
|
+
val safeEnd = end.coerceAtLeast(start).coerceAtMost(this.length)
|
|
31
|
+
|
|
32
|
+
return Pair(safeStart, safeEnd)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fun Spannable.getParagraphBounds(start: Int, end: Int): Pair<Int, Int> {
|
|
36
|
+
var startPosition = start.coerceAtLeast(0).coerceAtMost(this.length)
|
|
37
|
+
var endPosition = end.coerceAtLeast(0).coerceAtMost(this.length)
|
|
38
|
+
|
|
39
|
+
// Find the start of the paragraph
|
|
40
|
+
while (startPosition > 0 && this[startPosition - 1] != '\n') {
|
|
41
|
+
startPosition--
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Find the end of the paragraph
|
|
45
|
+
while (endPosition < this.length && this[endPosition] != '\n') {
|
|
46
|
+
endPosition++
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (startPosition >= endPosition) {
|
|
50
|
+
// If the start position is equal or greater than the end position, return the same position
|
|
51
|
+
startPosition = endPosition
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return Pair(startPosition, endPosition)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fun Spannable.getParagraphBounds(index: Int): Pair<Int, Int> {
|
|
58
|
+
return this.getParagraphBounds(index, index)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fun Spannable.mergeSpannables(start: Int, end: Int, string: String): Spannable {
|
|
62
|
+
return this.mergeSpannables(start, end, SpannableString(string))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fun Spannable.mergeSpannables(start: Int, end: Int, spannable: Spannable): Spannable {
|
|
66
|
+
var finalStart = start
|
|
67
|
+
var finalEnd = end
|
|
68
|
+
|
|
69
|
+
val builder = SpannableStringBuilder(this)
|
|
70
|
+
val startBlockSpans = spannable.getSpans(0, 0, EnrichedBlockSpan::class.java)
|
|
71
|
+
val startParagraphSpans = spannable.getSpans(0, 0, EnrichedParagraphSpan::class.java)
|
|
72
|
+
val endBlockSpans = spannable.getSpans(this.length, this.length, EnrichedBlockSpan::class.java)
|
|
73
|
+
val endParagraphSpans = spannable.getSpans(this.length, this.length, EnrichedParagraphSpan::class.java)
|
|
74
|
+
val (paragraphStart, paragraphEnd) = this.getParagraphBounds(start, end)
|
|
75
|
+
val isNewLineStart = startBlockSpans.isNotEmpty() || startParagraphSpans.isNotEmpty()
|
|
76
|
+
val isNewLineEnd = endBlockSpans.isNotEmpty() || endParagraphSpans.isNotEmpty()
|
|
77
|
+
|
|
78
|
+
if (isNewLineStart && start != paragraphStart) {
|
|
79
|
+
builder.insert(start, "\n")
|
|
80
|
+
finalStart = start + 1
|
|
81
|
+
finalEnd = end + 1
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (isNewLineEnd && end != paragraphEnd) {
|
|
85
|
+
builder.insert(finalEnd, "\n")
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
builder.replace(finalStart, finalEnd, spannable)
|
|
89
|
+
|
|
90
|
+
return builder
|
|
91
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
package com.swmansion.enriched.watchers
|
|
2
|
+
|
|
3
|
+
import android.text.SpanWatcher
|
|
4
|
+
import android.text.Spannable
|
|
5
|
+
import android.text.style.ParagraphStyle
|
|
6
|
+
import com.facebook.react.bridge.ReactContext
|
|
7
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
8
|
+
import com.swmansion.enriched.EnrichedTextInputView
|
|
9
|
+
import com.swmansion.enriched.events.OnChangeHtmlEvent
|
|
10
|
+
import com.swmansion.enriched.spans.EnrichedOrderedListSpan
|
|
11
|
+
import com.swmansion.enriched.spans.interfaces.EnrichedHeadingSpan
|
|
12
|
+
import com.swmansion.enriched.spans.interfaces.EnrichedSpan
|
|
13
|
+
import com.swmansion.enriched.utils.EnrichedParser
|
|
14
|
+
import com.swmansion.enriched.utils.getSafeSpanBoundaries
|
|
15
|
+
|
|
16
|
+
class EnrichedSpanWatcher(private val view: EnrichedTextInputView) : SpanWatcher {
|
|
17
|
+
private var previousHtml: String? = null
|
|
18
|
+
|
|
19
|
+
override fun onSpanAdded(text: Spannable, what: Any, start: Int, end: Int) {
|
|
20
|
+
updateNextLineLayout(what, text, end)
|
|
21
|
+
updateUnorderedListSpans(what, text, end)
|
|
22
|
+
emitEvent(text, what)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
override fun onSpanRemoved(text: Spannable, what: Any, start: Int, end: Int) {
|
|
26
|
+
updateNextLineLayout(what, text, end)
|
|
27
|
+
updateUnorderedListSpans(what, text, end)
|
|
28
|
+
emitEvent(text, what)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
override fun onSpanChanged(text: Spannable, what: Any, ostart: Int, oend: Int, nstart: Int, nend: Int) {
|
|
32
|
+
// Do nothing for now
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private fun updateUnorderedListSpans(what: Any, text: Spannable, end: Int) {
|
|
36
|
+
if (what is EnrichedOrderedListSpan) {
|
|
37
|
+
view.listStyles?.updateOrderedListIndexes(text, end)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// After adding/removing heading span, we have to manually set empty paragraph span to the following text
|
|
42
|
+
// This allows us to update the layout (as it's not updated automatically - looks like an Android issue)
|
|
43
|
+
private fun updateNextLineLayout(what: Any, text: Spannable, end: Int) {
|
|
44
|
+
class EmptySpan : ParagraphStyle {}
|
|
45
|
+
|
|
46
|
+
if (what is EnrichedHeadingSpan) {
|
|
47
|
+
val finalStart = (end + 1)
|
|
48
|
+
val finalEnd = text.length
|
|
49
|
+
val (safeStart, safeEnd) = text.getSafeSpanBoundaries(finalStart, finalEnd)
|
|
50
|
+
text.setSpan(EmptySpan(), safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fun emitEvent(s: Spannable, what: Any?) {
|
|
55
|
+
// Emit event only if we change one of ours spans
|
|
56
|
+
if (what != null && what !is EnrichedSpan) return
|
|
57
|
+
|
|
58
|
+
val html = EnrichedParser.toHtml(s)
|
|
59
|
+
if (html == previousHtml) return
|
|
60
|
+
|
|
61
|
+
previousHtml = html
|
|
62
|
+
view.layoutManager.invalidateLayout(view.text)
|
|
63
|
+
val context = view.context as ReactContext
|
|
64
|
+
val surfaceId = UIManagerHelper.getSurfaceId(context)
|
|
65
|
+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
|
|
66
|
+
dispatcher?.dispatchEvent(OnChangeHtmlEvent(
|
|
67
|
+
surfaceId,
|
|
68
|
+
view.id,
|
|
69
|
+
html,
|
|
70
|
+
view.experimentalSynchronousEvents,
|
|
71
|
+
))
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
package com.swmansion.enriched.watchers
|
|
2
|
+
|
|
3
|
+
import android.text.Editable
|
|
4
|
+
import android.text.TextWatcher
|
|
5
|
+
import com.facebook.react.bridge.ReactContext
|
|
6
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
7
|
+
import com.swmansion.enriched.EnrichedTextInputView
|
|
8
|
+
import com.swmansion.enriched.events.OnChangeTextEvent
|
|
9
|
+
|
|
10
|
+
class EnrichedTextWatcher(private val view: EnrichedTextInputView) : TextWatcher {
|
|
11
|
+
private var endCursorPosition: Int = 0
|
|
12
|
+
private var previousTextLength: Int = 0
|
|
13
|
+
|
|
14
|
+
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
|
15
|
+
previousTextLength = s?.length ?: 0
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
|
19
|
+
endCursorPosition = start + count
|
|
20
|
+
view.layoutManager.measureSize(s ?: "")
|
|
21
|
+
view.isRemovingMany = !view.isSettingValue && before > count + 1
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
override fun afterTextChanged(s: Editable?) {
|
|
25
|
+
if (s == null) return
|
|
26
|
+
emitEvents(s)
|
|
27
|
+
|
|
28
|
+
if (view.isSettingValue) return
|
|
29
|
+
applyStyles(s)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private fun applyStyles(s: Editable) {
|
|
33
|
+
view.inlineStyles?.afterTextChanged(s, endCursorPosition)
|
|
34
|
+
view.paragraphStyles?.afterTextChanged(s, endCursorPosition, previousTextLength)
|
|
35
|
+
view.listStyles?.afterTextChanged(s, endCursorPosition, previousTextLength)
|
|
36
|
+
view.parametrizedStyles?.afterTextChanged(s, endCursorPosition)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private fun emitEvents(s: Editable) {
|
|
40
|
+
val context = view.context as ReactContext
|
|
41
|
+
val surfaceId = UIManagerHelper.getSurfaceId(context)
|
|
42
|
+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
|
|
43
|
+
dispatcher?.dispatchEvent(OnChangeTextEvent(
|
|
44
|
+
surfaceId,
|
|
45
|
+
view.id,
|
|
46
|
+
s,
|
|
47
|
+
view.experimentalSynchronousEvents,
|
|
48
|
+
))
|
|
49
|
+
view.spanWatcher?.emitEvent(s, null)
|
|
50
|
+
}
|
|
51
|
+
}
|