react-native-enriched 0.1.6 → 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 +1 -1
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +1 -1
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +0 -45
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +19 -2
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +3 -3
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewPackage.kt +2 -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 +4 -0
- 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 +57 -30
- package/android/src/main/java/com/swmansion/enriched/utils/AsyncDrawable.kt +91 -0
- package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +16 -13
- 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 -0
- package/android/src/main/res/drawable/broken_image.xml +10 -0
- package/ios/EnrichedTextInputView.h +3 -0
- package/ios/EnrichedTextInputView.mm +97 -29
- package/ios/config/InputConfig.h +6 -0
- package/ios/config/InputConfig.mm +32 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +0 -45
- package/ios/generated/RNEnrichedTextInputViewSpec/RCTComponentViewHelpers.h +20 -4
- package/ios/inputParser/InputParser.mm +147 -24
- 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 +8 -0
- package/ios/styles/MentionStyle.mm +133 -36
- package/ios/styles/OrderedListStyle.mm +2 -0
- package/ios/styles/StrikethroughStyle.mm +2 -0
- package/ios/styles/UnderlineStyle.mm +2 -0
- package/ios/styles/UnorderedListStyle.mm +2 -0
- 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/StyleHeaders.h +12 -0
- package/ios/utils/ZeroWidthSpaceUtils.mm +22 -10
- package/lib/module/EnrichedTextInput.js +2 -2
- package/lib/module/EnrichedTextInput.js.map +1 -1
- package/lib/module/EnrichedTextInputNativeComponent.ts +6 -5
- package/lib/module/normalizeHtmlStyle.js +0 -4
- package/lib/module/normalizeHtmlStyle.js.map +1 -1
- package/lib/typescript/src/EnrichedTextInput.d.ts +1 -5
- package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +1 -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 +3 -7
- package/src/EnrichedTextInputNativeComponent.ts +6 -5
- package/src/normalizeHtmlStyle.ts +0 -4
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<img src="https://github.com/user-attachments/assets/
|
|
1
|
+
<img src="https://github.com/user-attachments/assets/b010571e-e4a3-4d92-a409-4f9fe614025d" alt="react-native-enriched by Software Mansion" width="100%">
|
|
2
2
|
|
|
3
3
|
# react-native-enriched
|
|
4
4
|
|
|
@@ -101,11 +101,11 @@ export default function App() {
|
|
|
101
101
|
<View style={styles.container}>
|
|
102
102
|
<EnrichedTextInput
|
|
103
103
|
ref={ref}
|
|
104
|
-
onChangeState={
|
|
104
|
+
onChangeState={e => setStylesState(e.nativeEvent)}
|
|
105
105
|
style={styles.input}
|
|
106
106
|
/>
|
|
107
107
|
<Button
|
|
108
|
-
title=
|
|
108
|
+
title={stylesState?.isBold ? 'Unbold' : 'Bold'}
|
|
109
109
|
color={stylesState?.isBold ? 'green' : 'gray'}
|
|
110
110
|
onPress={() => ref.current?.toggleBold()}
|
|
111
111
|
/>
|
|
@@ -152,9 +152,6 @@ Supported styles:
|
|
|
152
152
|
- ordered list
|
|
153
153
|
- unordered list
|
|
154
154
|
|
|
155
|
-
> [!NOTE]
|
|
156
|
-
> The iOS doesn't support codeblocks just yet, but it's planned in the near future!
|
|
157
|
-
|
|
158
155
|
Each of the styles can be toggled the same way as in the example from [usage section](#usage); call a proper `toggle` function on the component ref.
|
|
159
156
|
|
|
160
157
|
Each call toggles the style within the current text selection. We can still divide styles into two categories based on how they treat the selection:
|
|
@@ -212,9 +209,6 @@ You can insert an image into the input using [setImage](docs/API_REFERENCE.md#se
|
|
|
212
209
|
|
|
213
210
|
The image will be put into a single line in the input and will affect the line's height as well as input's height. Keep in mind, that image will replace currently selected text or insert into the cursor position if there is no text selection.
|
|
214
211
|
|
|
215
|
-
> [!NOTE]
|
|
216
|
-
> The iOS doesn't support inline images just yet, but it's planned in the near future!
|
|
217
|
-
|
|
218
212
|
## Style Detection
|
|
219
213
|
|
|
220
214
|
All of the above styles can be detected with the use of [onChangeState](docs/API_REFERENCE.md#onchangestate) event payload.
|
|
@@ -136,7 +136,7 @@ public class EnrichedTextInputViewManagerDelegate<T extends View, U extends Base
|
|
|
136
136
|
mViewManager.addLink(view, args.getInt(0), args.getInt(1), args.getString(2), args.getString(3));
|
|
137
137
|
break;
|
|
138
138
|
case "addImage":
|
|
139
|
-
mViewManager.addImage(view, args.getString(0));
|
|
139
|
+
mViewManager.addImage(view, args.getString(0), (float) args.getDouble(1), (float) args.getDouble(2));
|
|
140
140
|
break;
|
|
141
141
|
case "startMention":
|
|
142
142
|
mViewManager.startMention(view, args.getString(0));
|
|
@@ -50,7 +50,7 @@ public interface EnrichedTextInputViewManagerInterface<T extends View> extends V
|
|
|
50
50
|
void toggleOrderedList(T view);
|
|
51
51
|
void toggleUnorderedList(T view);
|
|
52
52
|
void addLink(T view, int start, int end, String text, String url);
|
|
53
|
-
void addImage(T view, String uri);
|
|
53
|
+
void addImage(T view, String uri, float width, float height);
|
|
54
54
|
void startMention(T view, String indicator);
|
|
55
55
|
void addMention(T view, String indicator, String text, String payload);
|
|
56
56
|
}
|
|
@@ -309,45 +309,6 @@ static inline folly::dynamic toDynamic(const EnrichedTextInputViewHtmlStyleAStru
|
|
|
309
309
|
}
|
|
310
310
|
#endif
|
|
311
311
|
|
|
312
|
-
struct EnrichedTextInputViewHtmlStyleImgStruct {
|
|
313
|
-
Float width{0.0};
|
|
314
|
-
Float height{0.0};
|
|
315
|
-
|
|
316
|
-
#ifdef RN_SERIALIZABLE_STATE
|
|
317
|
-
bool operator==(const EnrichedTextInputViewHtmlStyleImgStruct&) const = default;
|
|
318
|
-
|
|
319
|
-
folly::dynamic toDynamic() const {
|
|
320
|
-
folly::dynamic result = folly::dynamic::object();
|
|
321
|
-
result["width"] = width;
|
|
322
|
-
result["height"] = height;
|
|
323
|
-
return result;
|
|
324
|
-
}
|
|
325
|
-
#endif
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
static inline void fromRawValue(const PropsParserContext& context, const RawValue &value, EnrichedTextInputViewHtmlStyleImgStruct &result) {
|
|
329
|
-
auto map = (std::unordered_map<std::string, RawValue>)value;
|
|
330
|
-
|
|
331
|
-
auto tmp_width = map.find("width");
|
|
332
|
-
if (tmp_width != map.end()) {
|
|
333
|
-
fromRawValue(context, tmp_width->second, result.width);
|
|
334
|
-
}
|
|
335
|
-
auto tmp_height = map.find("height");
|
|
336
|
-
if (tmp_height != map.end()) {
|
|
337
|
-
fromRawValue(context, tmp_height->second, result.height);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
static inline std::string toString(const EnrichedTextInputViewHtmlStyleImgStruct &value) {
|
|
342
|
-
return "[Object EnrichedTextInputViewHtmlStyleImgStruct]";
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
#ifdef RN_SERIALIZABLE_STATE
|
|
346
|
-
static inline folly::dynamic toDynamic(const EnrichedTextInputViewHtmlStyleImgStruct &value) {
|
|
347
|
-
return value.toDynamic();
|
|
348
|
-
}
|
|
349
|
-
#endif
|
|
350
|
-
|
|
351
312
|
struct EnrichedTextInputViewHtmlStyleOlStruct {
|
|
352
313
|
Float gapWidth{0.0};
|
|
353
314
|
Float marginLeft{0.0};
|
|
@@ -459,7 +420,6 @@ struct EnrichedTextInputViewHtmlStyleStruct {
|
|
|
459
420
|
EnrichedTextInputViewHtmlStyleCodeStruct code{};
|
|
460
421
|
EnrichedTextInputViewHtmlStyleAStruct a{};
|
|
461
422
|
folly::dynamic mention{};
|
|
462
|
-
EnrichedTextInputViewHtmlStyleImgStruct img{};
|
|
463
423
|
EnrichedTextInputViewHtmlStyleOlStruct ol{};
|
|
464
424
|
EnrichedTextInputViewHtmlStyleUlStruct ul{};
|
|
465
425
|
|
|
@@ -476,7 +436,6 @@ struct EnrichedTextInputViewHtmlStyleStruct {
|
|
|
476
436
|
result["code"] = ::facebook::react::toDynamic(code);
|
|
477
437
|
result["a"] = ::facebook::react::toDynamic(a);
|
|
478
438
|
result["mention"] = mention;
|
|
479
|
-
result["img"] = ::facebook::react::toDynamic(img);
|
|
480
439
|
result["ol"] = ::facebook::react::toDynamic(ol);
|
|
481
440
|
result["ul"] = ::facebook::react::toDynamic(ul);
|
|
482
441
|
return result;
|
|
@@ -519,10 +478,6 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
|
|
|
519
478
|
if (tmp_mention != map.end()) {
|
|
520
479
|
fromRawValue(context, tmp_mention->second, result.mention);
|
|
521
480
|
}
|
|
522
|
-
auto tmp_img = map.find("img");
|
|
523
|
-
if (tmp_img != map.end()) {
|
|
524
|
-
fromRawValue(context, tmp_img->second, result.img);
|
|
525
|
-
}
|
|
526
481
|
auto tmp_ol = map.find("ol");
|
|
527
482
|
if (tmp_ol != map.end()) {
|
|
528
483
|
fromRawValue(context, tmp_ol->second, result.ol);
|
|
@@ -29,6 +29,7 @@ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
|
|
|
29
29
|
import com.swmansion.enriched.events.MentionHandler
|
|
30
30
|
import com.swmansion.enriched.events.OnInputBlurEvent
|
|
31
31
|
import com.swmansion.enriched.events.OnInputFocusEvent
|
|
32
|
+
import com.swmansion.enriched.spans.EnrichedImageSpan
|
|
32
33
|
import com.swmansion.enriched.spans.EnrichedSpans
|
|
33
34
|
import com.swmansion.enriched.styles.InlineStyles
|
|
34
35
|
import com.swmansion.enriched.styles.ListStyles
|
|
@@ -61,6 +62,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
61
62
|
var spanWatcher: EnrichedSpanWatcher? = null
|
|
62
63
|
var layoutManager: EnrichedTextInputViewLayoutManager = EnrichedTextInputViewLayoutManager(this)
|
|
63
64
|
|
|
65
|
+
var shouldEmitHtml: Boolean = true
|
|
64
66
|
var experimentalSynchronousEvents: Boolean = false
|
|
65
67
|
|
|
66
68
|
var fontSize: Float? = null
|
|
@@ -254,6 +256,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
254
256
|
val newText = parseText(value)
|
|
255
257
|
setText(newText)
|
|
256
258
|
|
|
259
|
+
observeAsyncImages()
|
|
257
260
|
// Assign SpanWatcher one more time as our previous spannable has been replaced
|
|
258
261
|
addSpanWatcher(EnrichedSpanWatcher(this))
|
|
259
262
|
|
|
@@ -262,6 +265,20 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
262
265
|
}
|
|
263
266
|
}
|
|
264
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Finds all async images in the current text and sets up listeners
|
|
270
|
+
* to redraw the text layout when they finish downloading.
|
|
271
|
+
*/
|
|
272
|
+
private fun observeAsyncImages() {
|
|
273
|
+
val liveText = text ?: return
|
|
274
|
+
|
|
275
|
+
val spans = liveText.getSpans(0, liveText.length, EnrichedImageSpan::class.java)
|
|
276
|
+
|
|
277
|
+
for (span in spans) {
|
|
278
|
+
span.observeAsyncDrawableLoaded(liveText)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
265
282
|
fun setAutoFocus(autoFocus: Boolean) {
|
|
266
283
|
this.autoFocus = autoFocus
|
|
267
284
|
}
|
|
@@ -527,11 +544,11 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
527
544
|
parametrizedStyles?.setLinkSpan(start, end, text, url)
|
|
528
545
|
}
|
|
529
546
|
|
|
530
|
-
fun addImage(src: String) {
|
|
547
|
+
fun addImage(src: String, width: Float, height: Float) {
|
|
531
548
|
val isValid = verifyStyle(EnrichedSpans.IMAGE)
|
|
532
549
|
if (!isValid) return
|
|
533
550
|
|
|
534
|
-
parametrizedStyles?.setImageSpan(src)
|
|
551
|
+
parametrizedStyles?.setImageSpan(src, width, height)
|
|
535
552
|
layoutManager.invalidateLayout()
|
|
536
553
|
}
|
|
537
554
|
|
|
@@ -177,7 +177,7 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
override fun setIsOnChangeHtmlSet(view: EnrichedTextInputView?, value: Boolean) {
|
|
180
|
-
|
|
180
|
+
view?.shouldEmitHtml = value
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
override fun setAutoCapitalize(view: EnrichedTextInputView?, flag: String?) {
|
|
@@ -255,8 +255,8 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
|
|
|
255
255
|
view?.addLink(start, end, text, url)
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
override fun addImage(view: EnrichedTextInputView?, src: String) {
|
|
259
|
-
view?.addImage(src)
|
|
258
|
+
override fun addImage(view: EnrichedTextInputView?, src: String, width: Float, height: Float) {
|
|
259
|
+
view?.addImage(src, width, height)
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
override fun startMention(view: EnrichedTextInputView?, indicator: String) {
|
|
@@ -4,10 +4,12 @@ import com.facebook.react.ReactPackage
|
|
|
4
4
|
import com.facebook.react.bridge.NativeModule
|
|
5
5
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
6
|
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
import com.swmansion.enriched.utils.ResourceManager
|
|
7
8
|
import java.util.ArrayList
|
|
8
9
|
|
|
9
10
|
class EnrichedTextInputViewPackage : ReactPackage {
|
|
10
11
|
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
12
|
+
ResourceManager.init(reactContext.applicationContext)
|
|
11
13
|
val viewManagers: MutableList<ViewManager<*, *>> = ArrayList()
|
|
12
14
|
viewManagers.add(EnrichedTextInputViewManager())
|
|
13
15
|
return viewManagers
|
|
@@ -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
|
}
|
|
@@ -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> {
|