react-native-enriched-markdown-scaffold 0.7.4-scaffold.3 → 0.7.4-scaffold.5
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/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt +12 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +8 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputLayoutManager.kt +30 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +27 -1
- package/ios/renderer/ListItemRenderer.m +8 -5
- package/package.json +1 -1
|
@@ -57,6 +57,13 @@ class EnrichedMarkdownTextInputManager :
|
|
|
57
57
|
stateWrapper: StateWrapper?,
|
|
58
58
|
): Any? {
|
|
59
59
|
view.stateWrapper = stateWrapper
|
|
60
|
+
// Fabric applies props before it hands over the state wrapper, so every layout
|
|
61
|
+
// invalidation raised during the mount transaction was dropped. Replay it now that we can
|
|
62
|
+
// actually push state — otherwise the shadow node keeps the initialMeasure() estimate,
|
|
63
|
+
// which ignores math chips, until the first keystroke.
|
|
64
|
+
if (stateWrapper != null) {
|
|
65
|
+
view.layoutManager.flushPendingInvalidation()
|
|
66
|
+
}
|
|
60
67
|
return super.updateState(view, props, stateWrapper)
|
|
61
68
|
}
|
|
62
69
|
|
|
@@ -69,6 +76,11 @@ class EnrichedMarkdownTextInputManager :
|
|
|
69
76
|
view.dismissActiveMention()
|
|
70
77
|
super.onDropViewInstance(view)
|
|
71
78
|
view.layoutManager.release()
|
|
79
|
+
// Unreachable while view recycling is off — it needs a `setupViewRecycling()` call from this
|
|
80
|
+
// constructor *and* ReactNativeFeatureFlags.enableViewRecycling(). Kept because the flush
|
|
81
|
+
// above turns a recycled view into a view that pushes state at mount: without this, it would
|
|
82
|
+
// push into the shadow node it was dropped from.
|
|
83
|
+
view.stateWrapper = null
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
override fun measure(
|
package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt
CHANGED
|
@@ -708,8 +708,16 @@ class EnrichedMarkdownTextInputView(
|
|
|
708
708
|
}
|
|
709
709
|
invalidate()
|
|
710
710
|
requestLayout()
|
|
711
|
+
// Chips only reach the text here when `defaultValue` landed before `markdownStyle`
|
|
712
|
+
// (@ReactProp order is undefined). requestLayout() cannot change a height Fabric owns —
|
|
713
|
+
// the shadow tree has to re-measure text that now carries the spans.
|
|
714
|
+
layoutManager.invalidateLayout()
|
|
711
715
|
} else if (mathStyleChanged) {
|
|
712
716
|
refreshAllMathChips()
|
|
717
|
+
// A math font size / padding change resizes every chip, so the measured height moves.
|
|
718
|
+
// Colour-only changes end up here too; store() then measures the same size and skips
|
|
719
|
+
// the state update.
|
|
720
|
+
layoutManager.invalidateLayout()
|
|
713
721
|
}
|
|
714
722
|
return changed
|
|
715
723
|
}
|
package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputLayoutManager.kt
CHANGED
|
@@ -8,21 +8,49 @@ class InputLayoutManager(
|
|
|
8
8
|
) {
|
|
9
9
|
private var forceHeightRecalculationCounter = 0
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* An invalidation that arrived before `stateWrapper` existed. Fabric applies every prop
|
|
13
|
+
* (and runs `onAfterUpdateTransaction`) inside `ViewManager.updateProperties`, which runs
|
|
14
|
+
* *before* `updateState` assigns the wrapper — so the whole mount transaction invalidates
|
|
15
|
+
* into the void. Without this flag the measurement store stays empty and Yoga keeps the
|
|
16
|
+
* `initialMeasure()` approximation until the next text/prop change.
|
|
17
|
+
*/
|
|
18
|
+
private var pending = false
|
|
19
|
+
|
|
11
20
|
fun invalidateLayout() {
|
|
12
|
-
|
|
21
|
+
val stateWrapper = view.stateWrapper
|
|
22
|
+
if (stateWrapper == null) {
|
|
23
|
+
pending = true
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
pending = false
|
|
13
27
|
|
|
14
28
|
val text = view.text
|
|
15
29
|
val paint = view.paint
|
|
16
30
|
|
|
31
|
+
// First call for this view measures against cachedWidth = 0 and stores a throwaway size;
|
|
32
|
+
// Yoga immediately re-measures through getMeasureById() with the real width and overwrites
|
|
33
|
+
// it. The state update below is what makes that second pass happen.
|
|
17
34
|
val needUpdate = InputMeasurementStore.store(view.id, text, paint)
|
|
18
35
|
if (!needUpdate) return
|
|
19
36
|
|
|
20
37
|
val state = Arguments.createMap()
|
|
21
38
|
state.putInt("forceHeightRecalculationCounter", forceHeightRecalculationCounter++)
|
|
22
|
-
|
|
39
|
+
stateWrapper.updateState(state)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Replays an invalidation that was dropped while `stateWrapper` was null. Called once the
|
|
44
|
+
* wrapper is assigned, i.e. after every prop of the mount transaction has been applied — so
|
|
45
|
+
* the text carries its math chips and the paint carries the resolved typeface/size.
|
|
46
|
+
*/
|
|
47
|
+
fun flushPendingInvalidation() {
|
|
48
|
+
if (!pending) return
|
|
49
|
+
invalidateLayout()
|
|
23
50
|
}
|
|
24
51
|
|
|
25
52
|
fun release() {
|
|
53
|
+
pending = false
|
|
26
54
|
InputMeasurementStore.release(view.id)
|
|
27
55
|
}
|
|
28
56
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
package com.swmansion.enriched.markdown.spans
|
|
2
2
|
|
|
3
3
|
import android.graphics.Paint
|
|
4
|
+
import android.text.Spanned
|
|
5
|
+
import android.text.style.ReplacementSpan
|
|
4
6
|
import kotlin.math.ceil
|
|
5
7
|
import kotlin.math.floor
|
|
6
8
|
import android.text.style.LineHeightSpan as AndroidLineHeightSpan
|
|
@@ -18,10 +20,34 @@ class LineHeightSpan(
|
|
|
18
20
|
v: Int,
|
|
19
21
|
fm: Paint.FontMetricsInt?,
|
|
20
22
|
) {
|
|
21
|
-
if (fm == null) return
|
|
23
|
+
if (fm == null || lineHeight <= 0) return
|
|
22
24
|
|
|
23
25
|
val leading = lineHeight - ((-fm.ascent) + fm.descent)
|
|
26
|
+
|
|
27
|
+
// A ReplacementSpan on this line — inline math, or the placeholder MeasurementStore
|
|
28
|
+
// swaps in to measure it off the main thread — reports its own metrics from getSize().
|
|
29
|
+
// Shrinking those back down to the configured line height cuts the line box below what
|
|
30
|
+
// the span actually draws, and can drive descent negative so the next line starts above
|
|
31
|
+
// this one's baseline. Those lines are expanded only, never compressed; every other line
|
|
32
|
+
// keeps the exact clamp it had before. The lookup only runs on the rare negative-leading
|
|
33
|
+
// path, so the common case stays allocation-free.
|
|
34
|
+
if (leading <= 0 && hasSelfMeasuringSpan(text, start, end)) return
|
|
35
|
+
|
|
24
36
|
fm.ascent -= ceil(leading / 2.0f).toInt()
|
|
25
37
|
fm.descent += floor(leading / 2.0f).toInt()
|
|
26
38
|
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Images are excluded on purpose. Block images already keep this span off their range via
|
|
42
|
+
* `applyLineHeightSkippingImages`, and inline ones never touch the metrics at all, so
|
|
43
|
+
* letting them through here would change image layout instead of math layout.
|
|
44
|
+
*/
|
|
45
|
+
private fun hasSelfMeasuringSpan(
|
|
46
|
+
text: CharSequence?,
|
|
47
|
+
start: Int,
|
|
48
|
+
end: Int,
|
|
49
|
+
): Boolean {
|
|
50
|
+
val spanned = text as? Spanned ?: return false
|
|
51
|
+
return spanned.getSpans(start, end, ReplacementSpan::class.java).any { it !is ImageSpan }
|
|
52
|
+
}
|
|
27
53
|
}
|
|
@@ -109,15 +109,18 @@ NSString *const TaskIndexAttribute = @"TaskIndex";
|
|
|
109
109
|
style.firstLineHeadIndent = totalIndent;
|
|
110
110
|
style.headIndent = totalIndent;
|
|
111
111
|
|
|
112
|
-
if (lineHeightConfig > 0) {
|
|
113
|
-
style.minimumLineHeight = lineHeightConfig;
|
|
114
|
-
style.maximumLineHeight = lineHeightConfig;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
112
|
NSMutableDictionary *attributesToApply = [metadata mutableCopy];
|
|
118
113
|
attributesToApply[NSParagraphStyleAttributeName] = style;
|
|
119
114
|
|
|
120
115
|
[output addAttributes:attributesToApply range:range];
|
|
116
|
+
|
|
117
|
+
// Line height goes through the shared helper so list items get the same math
|
|
118
|
+
// carve-out as paragraphs. Setting maximumLineHeight here directly would clamp
|
|
119
|
+
// the line fragment below the height an inline math attachment draws at, and the
|
|
120
|
+
// overflow paints over the neighbouring lines. applyLineHeight reads back the
|
|
121
|
+
// paragraph style we just applied, so the indents above survive, and it
|
|
122
|
+
// early-returns on a non-positive line height like the old guard did.
|
|
123
|
+
applyLineHeight(output, range, lineHeightConfig);
|
|
121
124
|
}];
|
|
122
125
|
|
|
123
126
|
if (isTask && isChecked) {
|
package/package.json
CHANGED