react-native-enriched 0.1.5 → 0.2.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 +3 -9
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +4 -1
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +2 -1
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +1 -45
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +53 -12
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewLayoutManager.kt +7 -56
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +19 -22
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewPackage.kt +2 -0
- package/android/src/main/java/com/swmansion/enriched/MeasurementStore.kt +158 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt +36 -1
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedImageSpan.kt +132 -11
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +65 -46
- package/android/src/main/java/com/swmansion/enriched/spans/utils/ForceRedrawSpan.kt +13 -0
- package/android/src/main/java/com/swmansion/enriched/styles/HtmlStyle.kt +2 -9
- package/android/src/main/java/com/swmansion/enriched/styles/InlineStyles.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/styles/ParagraphStyles.kt +110 -3
- package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +75 -32
- package/android/src/main/java/com/swmansion/enriched/utils/AsyncDrawable.kt +91 -0
- package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +38 -15
- package/android/src/main/java/com/swmansion/enriched/utils/ResourceManager.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedSpanWatcher.kt +3 -1
- package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt +1 -1
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp +15 -2
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h +1 -0
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp +1 -2
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/conversions.h +27 -0
- package/android/src/main/res/drawable/broken_image.xml +10 -0
- package/ios/EnrichedTextInputView.h +3 -1
- package/ios/EnrichedTextInputView.mm +167 -68
- package/ios/config/InputConfig.h +6 -0
- package/ios/config/InputConfig.mm +32 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +1 -45
- package/ios/generated/RNEnrichedTextInputViewSpec/RCTComponentViewHelpers.h +20 -4
- package/ios/inputParser/InputParser.mm +179 -31
- package/ios/inputTextView/InputTextView.mm +3 -5
- package/ios/internals/EnrichedTextInputViewShadowNode.h +1 -0
- package/ios/internals/EnrichedTextInputViewShadowNode.mm +29 -17
- package/ios/styles/BlockQuoteStyle.mm +5 -26
- package/ios/styles/BoldStyle.mm +2 -0
- package/ios/styles/CodeBlockStyle.mm +228 -0
- package/ios/styles/H1Style.mm +1 -0
- package/ios/styles/H2Style.mm +1 -0
- package/ios/styles/H3Style.mm +1 -0
- package/ios/styles/ImageStyle.mm +158 -0
- package/ios/styles/InlineCodeStyle.mm +2 -0
- package/ios/styles/ItalicStyle.mm +2 -0
- package/ios/styles/LinkStyle.mm +15 -7
- package/ios/styles/MentionStyle.mm +133 -36
- package/ios/styles/OrderedListStyle.mm +5 -8
- package/ios/styles/StrikethroughStyle.mm +2 -0
- package/ios/styles/UnderlineStyle.mm +2 -0
- package/ios/styles/UnorderedListStyle.mm +5 -8
- package/ios/utils/BaseStyleProtocol.h +1 -0
- package/ios/utils/ImageData.h +10 -0
- package/ios/utils/ImageData.mm +4 -0
- package/ios/utils/LayoutManagerExtension.mm +118 -3
- package/ios/utils/OccurenceUtils.h +4 -0
- package/ios/utils/OccurenceUtils.mm +47 -0
- package/ios/utils/ParagraphAttributesUtils.h +1 -0
- package/ios/utils/ParagraphAttributesUtils.mm +87 -20
- package/ios/utils/StringExtension.h +1 -1
- package/ios/utils/StringExtension.mm +17 -8
- package/ios/utils/StyleHeaders.h +12 -0
- package/ios/utils/ZeroWidthSpaceUtils.mm +22 -10
- package/lib/module/EnrichedTextInput.js +4 -2
- package/lib/module/EnrichedTextInput.js.map +1 -1
- package/lib/module/EnrichedTextInputNativeComponent.ts +7 -5
- package/lib/module/normalizeHtmlStyle.js +0 -4
- package/lib/module/normalizeHtmlStyle.js.map +1 -1
- package/lib/typescript/src/EnrichedTextInput.d.ts +3 -6
- package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +2 -5
- package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/normalizeHtmlStyle.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/EnrichedTextInput.tsx +6 -7
- package/src/EnrichedTextInputNativeComponent.ts +7 -5
- package/src/normalizeHtmlStyle.ts +0 -4
|
@@ -2,8 +2,10 @@ package com.swmansion.enriched.spans
|
|
|
2
2
|
|
|
3
3
|
import android.graphics.Canvas
|
|
4
4
|
import android.graphics.Paint
|
|
5
|
+
import android.graphics.Path
|
|
5
6
|
import android.graphics.RectF
|
|
6
7
|
import android.graphics.Typeface
|
|
8
|
+
import android.text.Spanned
|
|
7
9
|
import android.text.TextPaint
|
|
8
10
|
import android.text.style.LineBackgroundSpan
|
|
9
11
|
import android.text.style.MetricAffectingSpan
|
|
@@ -33,10 +35,43 @@ class EnrichedCodeBlockSpan(private val htmlStyle: HtmlStyle) : MetricAffectingS
|
|
|
33
35
|
end: Int,
|
|
34
36
|
lineNum: Int
|
|
35
37
|
) {
|
|
38
|
+
if (text !is Spanned) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
val previousColor = p.color
|
|
37
43
|
p.color = htmlStyle.codeBlockBackgroundColor
|
|
44
|
+
|
|
45
|
+
val radius = htmlStyle.codeBlockRadius
|
|
46
|
+
|
|
47
|
+
val spanStart = text.getSpanStart(this)
|
|
48
|
+
val spanEnd = text.getSpanEnd(this)
|
|
49
|
+
val isFirstLineOfSpan = start == spanStart
|
|
50
|
+
val isLastLineOfSpan = end == spanEnd || (spanEnd + 1 == end && text[spanEnd] == '\n')
|
|
51
|
+
|
|
52
|
+
val path = Path()
|
|
53
|
+
val radii = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f)
|
|
54
|
+
|
|
55
|
+
if (isFirstLineOfSpan) {
|
|
56
|
+
// Top-Left and Top-Right corners
|
|
57
|
+
radii[0] = radius
|
|
58
|
+
radii[1] = radius
|
|
59
|
+
radii[2] = radius
|
|
60
|
+
radii[3] = radius
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (isLastLineOfSpan) {
|
|
64
|
+
// Bottom-Right and Bottom-Left corners
|
|
65
|
+
radii[4] = radius
|
|
66
|
+
radii[5] = radius
|
|
67
|
+
radii[6] = radius
|
|
68
|
+
radii[7] = radius
|
|
69
|
+
}
|
|
70
|
+
|
|
38
71
|
val rect = RectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
|
|
39
|
-
|
|
72
|
+
|
|
73
|
+
path.addRoundRect(rect, radii, Path.Direction.CW)
|
|
74
|
+
canvas.drawPath(path, p)
|
|
40
75
|
p.color = previousColor
|
|
41
76
|
}
|
|
42
77
|
}
|
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
package com.swmansion.enriched.spans
|
|
2
2
|
|
|
3
|
-
import android.
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.content.res.Resources
|
|
5
|
+
import android.graphics.BitmapFactory
|
|
4
6
|
import android.graphics.Canvas
|
|
5
7
|
import android.graphics.Paint
|
|
8
|
+
import android.graphics.drawable.BitmapDrawable
|
|
6
9
|
import android.graphics.drawable.Drawable
|
|
7
|
-
import android.
|
|
10
|
+
import android.text.Editable
|
|
11
|
+
import android.text.Spannable
|
|
8
12
|
import android.text.style.ImageSpan
|
|
13
|
+
import android.util.Log
|
|
14
|
+
import androidx.core.graphics.drawable.DrawableCompat
|
|
9
15
|
import androidx.core.graphics.withSave
|
|
10
16
|
import com.swmansion.enriched.spans.interfaces.EnrichedInlineSpan
|
|
11
|
-
import com.swmansion.enriched.
|
|
17
|
+
import com.swmansion.enriched.utils.AsyncDrawable
|
|
18
|
+
import androidx.core.graphics.drawable.toDrawable
|
|
19
|
+
import com.swmansion.enriched.R
|
|
20
|
+
import com.swmansion.enriched.spans.utils.ForceRedrawSpan
|
|
21
|
+
import com.swmansion.enriched.utils.ResourceManager
|
|
12
22
|
|
|
13
23
|
class EnrichedImageSpan : ImageSpan, EnrichedInlineSpan {
|
|
14
|
-
private var
|
|
24
|
+
private var width: Int = 0
|
|
25
|
+
private var height: Int = 0
|
|
15
26
|
|
|
16
|
-
constructor(
|
|
17
|
-
this.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
constructor(drawable: Drawable, source: String, htmlStyle: HtmlStyle) : super(drawable, source, ALIGN_BASELINE) {
|
|
21
|
-
this.htmlStyle = htmlStyle
|
|
27
|
+
constructor(drawable: Drawable, source: String, width: Int, height: Int) : super(drawable, source, ALIGN_BASELINE) {
|
|
28
|
+
this.width = width
|
|
29
|
+
this.height = height
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
override fun draw(
|
|
@@ -35,7 +43,120 @@ class EnrichedImageSpan : ImageSpan, EnrichedInlineSpan {
|
|
|
35
43
|
|
|
36
44
|
override fun getDrawable(): Drawable {
|
|
37
45
|
val drawable = super.getDrawable()
|
|
38
|
-
|
|
46
|
+
val scale = Resources.getSystem().displayMetrics.density
|
|
47
|
+
|
|
48
|
+
drawable.setBounds(0, 0, (width * scale).toInt() , (height * scale).toInt())
|
|
39
49
|
return drawable
|
|
40
50
|
}
|
|
51
|
+
|
|
52
|
+
override fun getSize(
|
|
53
|
+
paint: Paint,
|
|
54
|
+
text: CharSequence?,
|
|
55
|
+
start: Int,
|
|
56
|
+
end: Int,
|
|
57
|
+
fm: Paint.FontMetricsInt?
|
|
58
|
+
): Int {
|
|
59
|
+
val d = drawable
|
|
60
|
+
val rect = d.bounds
|
|
61
|
+
|
|
62
|
+
if (fm != null) {
|
|
63
|
+
val imageHeight = rect.bottom - rect.top
|
|
64
|
+
|
|
65
|
+
// We want the image bottom to sit on the baseline (0).
|
|
66
|
+
// Therefore, the image top will be at: -imageHeight.
|
|
67
|
+
val targetTop = -imageHeight
|
|
68
|
+
|
|
69
|
+
// Expand the line UPWARDS if the image is taller than the current font
|
|
70
|
+
if (targetTop < fm.ascent) {
|
|
71
|
+
fm.ascent = targetTop
|
|
72
|
+
fm.top = targetTop
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return rect.right
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private fun registerDrawableLoadCallback (d: AsyncDrawable, text: Editable?) {
|
|
80
|
+
d.onLoaded = onLoaded@{
|
|
81
|
+
val spannable = text as? Spannable
|
|
82
|
+
|
|
83
|
+
if (spannable == null) {
|
|
84
|
+
return@onLoaded
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
val start = spannable.getSpanStart(this@EnrichedImageSpan)
|
|
88
|
+
val end = spannable.getSpanEnd(this@EnrichedImageSpan)
|
|
89
|
+
|
|
90
|
+
if (start != -1 && end != -1) {
|
|
91
|
+
// trick for adding empty span to force redraw when image is loaded
|
|
92
|
+
val redrawSpan = ForceRedrawSpan()
|
|
93
|
+
spannable.setSpan(redrawSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
94
|
+
spannable.removeSpan(redrawSpan)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fun observeAsyncDrawableLoaded(text: Editable?) {
|
|
100
|
+
val d = drawable
|
|
101
|
+
|
|
102
|
+
if (d !is AsyncDrawable) {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
registerDrawableLoadCallback(d, text)
|
|
107
|
+
|
|
108
|
+
// If it's already loaded (race condition), run logic immediately
|
|
109
|
+
if (d.isLoaded) {
|
|
110
|
+
d.onLoaded?.invoke()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fun getWidth(): Int {
|
|
115
|
+
return width
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fun getHeight(): Int {
|
|
119
|
+
return height
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
companion object {
|
|
123
|
+
fun createEnrichedImageSpan(src: String, width: Int, height: Int): EnrichedImageSpan {
|
|
124
|
+
var imgDrawable = prepareDrawableForImage(src)
|
|
125
|
+
|
|
126
|
+
if (imgDrawable == null) {
|
|
127
|
+
imgDrawable = ResourceManager.getDrawableResource(R.drawable.broken_image)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return EnrichedImageSpan(imgDrawable, src, width, height)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private fun prepareDrawableForImage(src: String): Drawable? {
|
|
134
|
+
var cleanPath = src
|
|
135
|
+
|
|
136
|
+
if (cleanPath.startsWith("http://") || cleanPath.startsWith("https://")) {
|
|
137
|
+
return AsyncDrawable(cleanPath)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (cleanPath.startsWith("file://")) {
|
|
141
|
+
cleanPath = cleanPath.substring(7)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
var drawable: BitmapDrawable? = null
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
val bitmap = BitmapFactory.decodeFile(cleanPath)
|
|
148
|
+
if (bitmap != null) {
|
|
149
|
+
drawable = bitmap.toDrawable(Resources.getSystem())
|
|
150
|
+
// set bounds so it knows how big it is naturally,
|
|
151
|
+
// though EnrichedImageSpan will override this with the HTML width/height later.
|
|
152
|
+
drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight())
|
|
153
|
+
}
|
|
154
|
+
} catch (e: Exception) {
|
|
155
|
+
// Failed to load file
|
|
156
|
+
Log.e("EnrichedImageSpan", "Failed to load image from path: $cleanPath", e)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return drawable
|
|
160
|
+
}
|
|
161
|
+
}
|
|
41
162
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package com.swmansion.enriched.spans
|
|
2
2
|
|
|
3
|
+
import com.swmansion.enriched.styles.HtmlStyle
|
|
4
|
+
|
|
3
5
|
data class BaseSpanConfig(val clazz: Class<*>)
|
|
4
6
|
data class ParagraphSpanConfig(val clazz: Class<*>, val isContinuous: Boolean)
|
|
5
7
|
data class ListSpanConfig(val clazz: Class<*>, val shortcut: String)
|
|
@@ -62,50 +64,67 @@ object EnrichedSpans {
|
|
|
62
64
|
MENTION to BaseSpanConfig(EnrichedMentionSpan::class.java),
|
|
63
65
|
)
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
67
|
+
fun getMergingConfigForStyle(style: String, htmlStyle: HtmlStyle): StylesMergingConfig? {
|
|
68
|
+
return when (style) {
|
|
69
|
+
BOLD -> {
|
|
70
|
+
val blockingStyles = mutableListOf(CODE_BLOCK)
|
|
71
|
+
if (htmlStyle.h1Bold) blockingStyles.add(H1)
|
|
72
|
+
if (htmlStyle.h2Bold) blockingStyles.add(H2)
|
|
73
|
+
if (htmlStyle.h3Bold) blockingStyles.add(H3)
|
|
74
|
+
StylesMergingConfig(blockingStyles = blockingStyles.toTypedArray())
|
|
75
|
+
}
|
|
76
|
+
ITALIC -> StylesMergingConfig(
|
|
77
|
+
blockingStyles = arrayOf(CODE_BLOCK)
|
|
78
|
+
)
|
|
79
|
+
UNDERLINE -> StylesMergingConfig(
|
|
80
|
+
blockingStyles = arrayOf(CODE_BLOCK)
|
|
81
|
+
)
|
|
82
|
+
STRIKETHROUGH -> StylesMergingConfig(
|
|
83
|
+
blockingStyles = arrayOf(CODE_BLOCK)
|
|
84
|
+
)
|
|
85
|
+
INLINE_CODE -> StylesMergingConfig(
|
|
86
|
+
conflictingStyles = arrayOf(MENTION, LINK),
|
|
87
|
+
blockingStyles = arrayOf(CODE_BLOCK)
|
|
88
|
+
)
|
|
89
|
+
H1 -> {
|
|
90
|
+
val conflictingStyles = mutableListOf(H2, H3, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
|
|
91
|
+
if (htmlStyle.h1Bold) conflictingStyles.add(BOLD)
|
|
92
|
+
StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
|
|
93
|
+
}
|
|
94
|
+
H2 -> {
|
|
95
|
+
val conflictingStyles = mutableListOf(H1, H3, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
|
|
96
|
+
if (htmlStyle.h2Bold) conflictingStyles.add(BOLD)
|
|
97
|
+
StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
|
|
98
|
+
}
|
|
99
|
+
H3 -> {
|
|
100
|
+
val conflictingStyles = mutableListOf(H1, H2, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
|
|
101
|
+
if (htmlStyle.h3Bold) conflictingStyles.add(BOLD)
|
|
102
|
+
StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
|
|
103
|
+
}
|
|
104
|
+
BLOCK_QUOTE -> StylesMergingConfig(
|
|
105
|
+
conflictingStyles = arrayOf(H1, H2, H3, CODE_BLOCK, ORDERED_LIST, UNORDERED_LIST)
|
|
106
|
+
)
|
|
107
|
+
CODE_BLOCK -> StylesMergingConfig(
|
|
108
|
+
conflictingStyles = arrayOf(H1, H2, H3, BOLD, ITALIC, UNDERLINE, STRIKETHROUGH, UNORDERED_LIST, ORDERED_LIST, BLOCK_QUOTE, INLINE_CODE)
|
|
109
|
+
)
|
|
110
|
+
UNORDERED_LIST -> StylesMergingConfig(
|
|
111
|
+
conflictingStyles = arrayOf(H1, H2, H3, ORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE)
|
|
112
|
+
)
|
|
113
|
+
ORDERED_LIST -> StylesMergingConfig(
|
|
114
|
+
conflictingStyles = arrayOf(H1, H2, H3, UNORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE),
|
|
115
|
+
)
|
|
116
|
+
LINK -> StylesMergingConfig(
|
|
117
|
+
blockingStyles = arrayOf(INLINE_CODE, CODE_BLOCK, MENTION)
|
|
118
|
+
)
|
|
119
|
+
IMAGE -> StylesMergingConfig()
|
|
120
|
+
MENTION -> StylesMergingConfig(
|
|
121
|
+
blockingStyles = arrayOf(INLINE_CODE, CODE_BLOCK, LINK)
|
|
122
|
+
)
|
|
123
|
+
else -> null
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fun isTypeContinuous(type: Class<*>): Boolean {
|
|
128
|
+
return paragraphSpans.values.find { it.clazz == type }?.isContinuous == true
|
|
129
|
+
}
|
|
111
130
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package com.swmansion.enriched.spans.utils
|
|
2
|
+
|
|
3
|
+
import android.text.TextPaint
|
|
4
|
+
import android.text.style.MetricAffectingSpan
|
|
5
|
+
|
|
6
|
+
class ForceRedrawSpan: MetricAffectingSpan() {
|
|
7
|
+
override fun updateMeasureState(tp: TextPaint) {
|
|
8
|
+
// Do nothing, we don't actually want to change how it looks
|
|
9
|
+
}
|
|
10
|
+
override fun updateDrawState(tp: TextPaint?) {
|
|
11
|
+
// Do nothing
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -43,9 +43,6 @@ class HtmlStyle {
|
|
|
43
43
|
var ulBulletSize: Int = 8
|
|
44
44
|
var ulBulletColor: Int = Color.BLACK
|
|
45
45
|
|
|
46
|
-
var imgWidth: Int = 200
|
|
47
|
-
var imgHeight: Int = 200
|
|
48
|
-
|
|
49
46
|
var aColor: Int = Color.BLACK
|
|
50
47
|
var aUnderline: Boolean = true
|
|
51
48
|
|
|
@@ -100,10 +97,6 @@ class HtmlStyle {
|
|
|
100
97
|
ulMarginLeft = parseFloat(ulStyle, "marginLeft").toInt()
|
|
101
98
|
ulBulletSize = parseFloat(ulStyle, "bulletSize").toInt()
|
|
102
99
|
|
|
103
|
-
val imgStyle = style.getMap("img")
|
|
104
|
-
imgWidth = parseFloat(imgStyle, "width").toInt()
|
|
105
|
-
imgHeight = parseFloat(imgStyle, "height").toInt()
|
|
106
|
-
|
|
107
100
|
val aStyle = style.getMap("a")
|
|
108
101
|
aColor = parseColor(aStyle, "color")
|
|
109
102
|
aUnderline = parseIsUnderline(aStyle)
|
|
@@ -124,8 +117,8 @@ class HtmlStyle {
|
|
|
124
117
|
private fun parseFloat(map: ReadableMap?, key: String): Float {
|
|
125
118
|
val safeMap = ensureValueIsSet(map, key)
|
|
126
119
|
|
|
127
|
-
val
|
|
128
|
-
return ceil(PixelUtil.toPixelFromSP(
|
|
120
|
+
val value = safeMap.getDouble(key)
|
|
121
|
+
return ceil(PixelUtil.toPixelFromSP(value))
|
|
129
122
|
}
|
|
130
123
|
|
|
131
124
|
private fun parseColorWithOpacity(map: ReadableMap?, key: String, opacity: Int): Int {
|
|
@@ -3,13 +3,86 @@ package com.swmansion.enriched.styles
|
|
|
3
3
|
import android.text.Editable
|
|
4
4
|
import android.text.Spannable
|
|
5
5
|
import android.text.SpannableStringBuilder
|
|
6
|
+
import android.util.Log
|
|
6
7
|
import com.swmansion.enriched.EnrichedTextInputView
|
|
8
|
+
import com.swmansion.enriched.spans.EnrichedBlockQuoteSpan
|
|
9
|
+
import com.swmansion.enriched.spans.EnrichedCodeBlockSpan
|
|
7
10
|
import com.swmansion.enriched.spans.EnrichedSpans
|
|
8
11
|
import com.swmansion.enriched.utils.getParagraphBounds
|
|
9
12
|
import com.swmansion.enriched.utils.getSafeSpanBoundaries
|
|
10
13
|
|
|
11
14
|
class ParagraphStyles(private val view: EnrichedTextInputView) {
|
|
15
|
+
private fun <T>getPreviousParagraphSpan(spannable: Spannable, paragraphStart: Int, type: Class<T>): T? {
|
|
16
|
+
if (paragraphStart <= 0) return null
|
|
17
|
+
|
|
18
|
+
val (previousParagraphStart, previousParagraphEnd) = spannable.getParagraphBounds(paragraphStart - 1)
|
|
19
|
+
val spans = spannable.getSpans(previousParagraphStart, previousParagraphEnd, type)
|
|
20
|
+
|
|
21
|
+
// A paragraph implies a single cohesive style. having multiple spans of the
|
|
22
|
+
// same type (e.g., two codeblock spans) in one paragraph is an invalid state in current library logic
|
|
23
|
+
if (spans.size > 1) {
|
|
24
|
+
Log.w("ParagraphStyles", "getPreviousParagraphSpan(): Found more than one span in the paragraph!")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (spans.isNotEmpty()) {
|
|
28
|
+
return spans.first()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private fun <T>getNextParagraphSpan(spannable: Spannable, paragraphEnd: Int, type: Class<T>): T? {
|
|
35
|
+
if (paragraphEnd >= spannable.length - 1) return null
|
|
36
|
+
|
|
37
|
+
val (nextParagraphStart, nextParagraphEnd) = spannable.getParagraphBounds(paragraphEnd + 1)
|
|
38
|
+
|
|
39
|
+
val spans = spannable.getSpans(nextParagraphStart, nextParagraphEnd, type)
|
|
40
|
+
|
|
41
|
+
// A paragraph implies a single cohesive style. having multiple spans of the
|
|
42
|
+
// same type (e.g., two codeblock spans) in one paragraph is an invalid state in current library logic
|
|
43
|
+
if (spans.size > 1) {
|
|
44
|
+
Log.w("ParagraphStyles", "getNextParagraphSpan(): Found more than one span in the paragraph!")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (spans.isNotEmpty()) {
|
|
48
|
+
return spans.first()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Applies a continuous span to the specified range.
|
|
56
|
+
* If the new range touches existing continuous spans, they are coalesced into a single span
|
|
57
|
+
*/
|
|
58
|
+
private fun <T>setContinuousSpan(spannable: Spannable, start: Int, end: Int, type: Class<T>) {
|
|
59
|
+
val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle)
|
|
60
|
+
val previousSpan = getPreviousParagraphSpan(spannable, start, type)
|
|
61
|
+
val nextSpan = getNextParagraphSpan(spannable, end, type)
|
|
62
|
+
var newStart = start
|
|
63
|
+
var newEnd = end
|
|
64
|
+
|
|
65
|
+
if (previousSpan != null) {
|
|
66
|
+
newStart = spannable.getSpanStart(previousSpan)
|
|
67
|
+
spannable.removeSpan(previousSpan)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (nextSpan != null && start != end) {
|
|
71
|
+
newEnd = spannable.getSpanEnd(nextSpan)
|
|
72
|
+
spannable.removeSpan(nextSpan)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(newStart, newEnd)
|
|
76
|
+
spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
12
80
|
private fun <T>setSpan(spannable: Spannable, type: Class<T>, start: Int, end: Int) {
|
|
81
|
+
if (EnrichedSpans.isTypeContinuous(type)) {
|
|
82
|
+
setContinuousSpan(spannable, start, end, type)
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
13
86
|
val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle)
|
|
14
87
|
val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, end)
|
|
15
88
|
spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
@@ -94,6 +167,33 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
|
|
|
94
167
|
return spans.isNotEmpty()
|
|
95
168
|
}
|
|
96
169
|
|
|
170
|
+
private fun <T>mergeAdjacentStyleSpans(s: Editable, endCursorPosition: Int, type: Class<T>) {
|
|
171
|
+
val (start, end) = s.getParagraphBounds(endCursorPosition)
|
|
172
|
+
val currParagraphSpans = s.getSpans(start, end, type)
|
|
173
|
+
|
|
174
|
+
if (currParagraphSpans.isEmpty()) {
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
val currSpan = currParagraphSpans[0]
|
|
179
|
+
val nextSpan = getNextParagraphSpan(s, end, type)
|
|
180
|
+
|
|
181
|
+
if (nextSpan == null) {
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
val newStart = s.getSpanStart(currSpan)
|
|
186
|
+
val newEnd = s.getSpanEnd(nextSpan)
|
|
187
|
+
|
|
188
|
+
s.removeSpan(nextSpan)
|
|
189
|
+
s.removeSpan(currSpan)
|
|
190
|
+
|
|
191
|
+
val (safeStart, safeEnd) = s.getSafeSpanBoundaries(newStart, newEnd)
|
|
192
|
+
val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle)
|
|
193
|
+
|
|
194
|
+
s.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
195
|
+
}
|
|
196
|
+
|
|
97
197
|
fun afterTextChanged(s: Editable, endPosition: Int, previousTextLength: Int) {
|
|
98
198
|
var endCursorPosition = endPosition
|
|
99
199
|
val isBackspace = s.length < previousTextLength
|
|
@@ -101,7 +201,14 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
|
|
|
101
201
|
|
|
102
202
|
for ((style, config) in EnrichedSpans.paragraphSpans) {
|
|
103
203
|
val spanState = view.spanState ?: continue
|
|
104
|
-
val styleStart = spanState.getStart(style)
|
|
204
|
+
val styleStart = spanState.getStart(style)
|
|
205
|
+
|
|
206
|
+
if (styleStart == null) {
|
|
207
|
+
if (config.isContinuous) {
|
|
208
|
+
mergeAdjacentStyleSpans(s, endCursorPosition, config.clazz)
|
|
209
|
+
}
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
105
212
|
|
|
106
213
|
if (isNewLine) {
|
|
107
214
|
if (!config.isContinuous) {
|
|
@@ -154,8 +261,8 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
|
|
|
154
261
|
|
|
155
262
|
if (start == end) {
|
|
156
263
|
spannable.insert(start, "\u200B")
|
|
157
|
-
view.spanState?.setStart(name, start + 1)
|
|
158
264
|
setAndMergeSpans(spannable, type, start, end + 1)
|
|
265
|
+
view.selection.validateStyles()
|
|
159
266
|
|
|
160
267
|
return
|
|
161
268
|
}
|
|
@@ -170,8 +277,8 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
|
|
|
170
277
|
currentStart = currentEnd + 1
|
|
171
278
|
}
|
|
172
279
|
|
|
173
|
-
view.spanState?.setStart(name, start)
|
|
174
280
|
setAndMergeSpans(spannable, type, start, currentEnd)
|
|
281
|
+
view.selection.validateStyles()
|
|
175
282
|
}
|
|
176
283
|
|
|
177
284
|
fun getStyleRange(): Pair<Int, Int> {
|