react-native-advanced-text 0.1.43 → 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/AdvancedText.podspec +18 -1
- package/README.md +32 -1
- package/android/CMakeLists.txt +51 -0
- package/android/src/main/java/com/advancedtext/AdvancedTextView.kt +48 -5
- package/android/src/main/java/com/advancedtext/AdvancedTextViewManager.kt +14 -0
- package/common/cpp/react/renderer/components/AdvancedTextViewSpec/ShadowNodes.cpp +120 -0
- package/common/cpp/react/renderer/components/AdvancedTextViewSpec/ShadowNodes.h +95 -0
- package/ios/AdvancedTextView.mm +59 -5
- package/lib/module/AdvancedTextViewNativeComponent.ts +2 -0
- package/lib/typescript/src/AdvancedTextViewNativeComponent.d.ts +2 -0
- package/lib/typescript/src/AdvancedTextViewNativeComponent.d.ts.map +1 -1
- package/package.json +2 -1
- package/react-native.config.js +22 -0
- package/src/AdvancedTextViewNativeComponent.ts +2 -0
package/AdvancedText.podspec
CHANGED
|
@@ -13,8 +13,25 @@ Pod::Spec.new do |s|
|
|
|
13
13
|
s.platforms = { :ios => min_ios_version_supported }
|
|
14
14
|
s.source = { :git => "https://github.com/AminAllahham/react-native-advanced-text.git", :tag => "#{s.version}" }
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# `common/cpp` holds the self-measuring Fabric ShadowNode. Its
|
|
17
|
+
# ShadowNodes.h intentionally shadows the codegen output, so `common/cpp`
|
|
18
|
+
# must come first on the header search path (see below).
|
|
19
|
+
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}", "common/cpp/**/*.{h,cpp}"
|
|
17
20
|
s.private_header_files = "ios/**/*.h"
|
|
18
21
|
|
|
19
22
|
install_modules_dependencies(s)
|
|
23
|
+
|
|
24
|
+
# `install_modules_dependencies` sets `s.pod_target_xcconfig`; extend it
|
|
25
|
+
# (rather than replace it) so our headers win over the generated ones and
|
|
26
|
+
# so `React-FabricComponents` headers (TextLayoutManager) resolve.
|
|
27
|
+
xcconfig = s.attributes_hash["pod_target_xcconfig"] || {}
|
|
28
|
+
existing_header_paths = xcconfig["HEADER_SEARCH_PATHS"] || "$(inherited)"
|
|
29
|
+
xcconfig["HEADER_SEARCH_PATHS"] =
|
|
30
|
+
"\"$(PODS_TARGET_SRCROOT)/common/cpp\" #{existing_header_paths}"
|
|
31
|
+
xcconfig["CLANG_CXX_LANGUAGE_STANDARD"] ||= "c++20"
|
|
32
|
+
s.pod_target_xcconfig = xcconfig
|
|
33
|
+
|
|
34
|
+
# Needed for <react/renderer/textlayoutmanager/TextLayoutManager.h>, which is
|
|
35
|
+
# not pulled in by `install_modules_dependencies`.
|
|
36
|
+
s.dependency "React-FabricComponents"
|
|
20
37
|
end
|
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
npm install react-native-advanced-text
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
> Requires the New Architecture (Fabric). React Native 0.80+ recommended.
|
|
12
13
|
|
|
13
14
|
## Usage
|
|
14
15
|
|
|
@@ -18,7 +19,7 @@ import { AdvancedText } from "react-native-advanced-text";
|
|
|
18
19
|
|
|
19
20
|
<AdvancedText
|
|
20
21
|
text={'This is an example of AdvancedText component. Tap on any word to see the event in action.'}
|
|
21
|
-
style={
|
|
22
|
+
style={styles.AdvancedText}
|
|
22
23
|
onWordPress={(event) => {
|
|
23
24
|
console.log({event})
|
|
24
25
|
}}
|
|
@@ -42,9 +43,39 @@ import { AdvancedText } from "react-native-advanced-text";
|
|
|
42
43
|
color={'#FFFFFF'}
|
|
43
44
|
fontWeight="normal"
|
|
44
45
|
fontFamily={'monospace'}
|
|
46
|
+
letterSpacing={1}
|
|
45
47
|
/>
|
|
46
48
|
```
|
|
47
49
|
|
|
50
|
+
## Sizing
|
|
51
|
+
|
|
52
|
+
`AdvancedText` measures its own height, like React Native's built‑in `<Text>`.
|
|
53
|
+
Give it a width (via `style`, `flex`, or a constraining parent) and it derives
|
|
54
|
+
its height from the text content and that width — **no explicit `height` is
|
|
55
|
+
required**.
|
|
56
|
+
|
|
57
|
+
Under the hood the Fabric ShadowNode implements `measureContent()` using React
|
|
58
|
+
Native's own `TextLayoutManager` (the same engine `<Text>` uses), so wrapping,
|
|
59
|
+
font metrics, `letterSpacing` and `lineHeight` are accounted for on both iOS and
|
|
60
|
+
Android without any JS‑side text measurement.
|
|
61
|
+
|
|
62
|
+
```jsx
|
|
63
|
+
// Before: an explicit height had to be estimated and passed in.
|
|
64
|
+
<AdvancedText text={text} style={{ height: estimatedHeight }} />
|
|
65
|
+
|
|
66
|
+
// Now:
|
|
67
|
+
<AdvancedText text={text} style={{ width: '100%' }} />
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Props that affect measurement
|
|
71
|
+
|
|
72
|
+
| Prop | Notes |
|
|
73
|
+
| --------------- | ------------------------------------------------------------------- |
|
|
74
|
+
| `fontSize` | points (defaults to 16) |
|
|
75
|
+
| `fontFamily` | |
|
|
76
|
+
| `fontWeight` | `"bold"` or `"italic"` |
|
|
77
|
+
| `letterSpacing` | points; applied to both rendering and measurement |
|
|
78
|
+
| `lineHeight` | multiple of the font size (unchanged behavior) |
|
|
48
79
|
|
|
49
80
|
## Contributing
|
|
50
81
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Custom CMake build for react-native-advanced-text.
|
|
2
|
+
#
|
|
3
|
+
# This replaces the auto-generated
|
|
4
|
+
# build/generated/source/codegen/jni/CMakeLists.txt
|
|
5
|
+
# (referenced from react-native.config.js via `cmakeListsPath`) so that the
|
|
6
|
+
# Fabric component is backed by our self-measuring ShadowNode
|
|
7
|
+
# (common/cpp/react/renderer/components/AdvancedTextViewSpec/ShadowNodes.{h,cpp})
|
|
8
|
+
# instead of the plain, non-measuring one that codegen emits.
|
|
9
|
+
#
|
|
10
|
+
# It still produces the target name the app autolinking expects:
|
|
11
|
+
# react_codegen_AdvancedTextViewSpec
|
|
12
|
+
#
|
|
13
|
+
# The key detail is include order: `common/cpp` is listed BEFORE the codegen
|
|
14
|
+
# output dir, so `#include <react/renderer/components/AdvancedTextViewSpec/ShadowNodes.h>`
|
|
15
|
+
# resolves to our version, while Props.h / EventEmitters.h / States.h still
|
|
16
|
+
# resolve to the generated ones.
|
|
17
|
+
|
|
18
|
+
cmake_minimum_required(VERSION 3.13)
|
|
19
|
+
set(CMAKE_VERBOSE_MAKEFILE ON)
|
|
20
|
+
|
|
21
|
+
set(CODEGEN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/build/generated/source/codegen/jni")
|
|
22
|
+
set(COMMON_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../common/cpp")
|
|
23
|
+
|
|
24
|
+
file(GLOB codegen_SRCS CONFIGURE_DEPENDS
|
|
25
|
+
"${CODEGEN_DIR}/*.cpp"
|
|
26
|
+
"${CODEGEN_DIR}/react/renderer/components/AdvancedTextViewSpec/*.cpp")
|
|
27
|
+
|
|
28
|
+
file(GLOB custom_SRCS CONFIGURE_DEPENDS
|
|
29
|
+
"${COMMON_DIR}/react/renderer/components/AdvancedTextViewSpec/*.cpp")
|
|
30
|
+
|
|
31
|
+
add_library(
|
|
32
|
+
react_codegen_AdvancedTextViewSpec
|
|
33
|
+
OBJECT
|
|
34
|
+
${codegen_SRCS}
|
|
35
|
+
${custom_SRCS}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# `common/cpp` MUST come first so our ShadowNodes.h shadows the generated one.
|
|
39
|
+
target_include_directories(react_codegen_AdvancedTextViewSpec PUBLIC
|
|
40
|
+
"${COMMON_DIR}"
|
|
41
|
+
"${CODEGEN_DIR}"
|
|
42
|
+
"${CODEGEN_DIR}/react/renderer/components/AdvancedTextViewSpec")
|
|
43
|
+
|
|
44
|
+
target_link_libraries(
|
|
45
|
+
react_codegen_AdvancedTextViewSpec
|
|
46
|
+
fbjni
|
|
47
|
+
jsi
|
|
48
|
+
reactnative
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
target_compile_reactnative_options(react_codegen_AdvancedTextViewSpec PRIVATE)
|
|
@@ -41,9 +41,12 @@ class AdvancedTextView : TextView {
|
|
|
41
41
|
private var textAlign: String = "left"
|
|
42
42
|
private var fontFamily: String = "sans-serif"
|
|
43
43
|
private var lineHeightMultiplier: Float = 1.0f
|
|
44
|
+
// Value from JS, in dp/points (0 = unset). Converted to em for TextView.
|
|
45
|
+
private var letterSpacingDp: Float = 0f
|
|
44
46
|
|
|
45
47
|
private var indicatorWordIndex: Int = -1
|
|
46
48
|
private var indicatorColor: String = ""
|
|
49
|
+
private var indicatorMode: String = "Highlight"
|
|
47
50
|
|
|
48
51
|
private var wordPositions: List<WordPosition> = emptyList()
|
|
49
52
|
|
|
@@ -55,7 +58,10 @@ class AdvancedTextView : TextView {
|
|
|
55
58
|
Log.d(TAG, "AdvancedTextView initialized")
|
|
56
59
|
|
|
57
60
|
textSize = 16f
|
|
58
|
-
|
|
61
|
+
// Zero intrinsic padding so the rendered text matches the size reported
|
|
62
|
+
// by the Fabric ShadowNode's measureContent(). Consumers add padding via
|
|
63
|
+
// React Native `style` instead, exactly like <Text>.
|
|
64
|
+
setPadding(0, 0, 0, 0)
|
|
59
65
|
setTextIsSelectable(true)
|
|
60
66
|
|
|
61
67
|
movementMethod = SmartMovementMethod
|
|
@@ -147,6 +153,12 @@ class AdvancedTextView : TextView {
|
|
|
147
153
|
updateTextWithHighlights()
|
|
148
154
|
}
|
|
149
155
|
|
|
156
|
+
fun setAdvancedLetterSpacing(spacingDp: Float) {
|
|
157
|
+
if (letterSpacingDp == spacingDp) return
|
|
158
|
+
letterSpacingDp = spacingDp
|
|
159
|
+
updateTextWithHighlights()
|
|
160
|
+
}
|
|
161
|
+
|
|
150
162
|
|
|
151
163
|
fun setAdvancedFontFamily(family: String) {
|
|
152
164
|
if (fontFamily == family) return
|
|
@@ -162,12 +174,27 @@ class AdvancedTextView : TextView {
|
|
|
162
174
|
fun setIndicatorWordIndex(index: Int) {
|
|
163
175
|
if (indicatorWordIndex == index) return
|
|
164
176
|
indicatorWordIndex = index
|
|
165
|
-
|
|
177
|
+
if (indicatorMode == "fill") {
|
|
178
|
+
updateTextWithHighlights()
|
|
179
|
+
} else {
|
|
180
|
+
invalidate()
|
|
181
|
+
}
|
|
166
182
|
}
|
|
167
183
|
|
|
168
184
|
fun setIndicatorColor(color: String) {
|
|
169
185
|
if (indicatorColor == color) return
|
|
170
186
|
indicatorColor = color
|
|
187
|
+
if (indicatorMode == "fill") {
|
|
188
|
+
updateTextWithHighlights()
|
|
189
|
+
} else {
|
|
190
|
+
invalidate()
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fun setIndicatorMode(mode: String) {
|
|
195
|
+
if (indicatorMode == mode) return
|
|
196
|
+
indicatorMode = mode
|
|
197
|
+
updateTextWithHighlights()
|
|
171
198
|
invalidate()
|
|
172
199
|
}
|
|
173
200
|
|
|
@@ -214,11 +241,19 @@ class AdvancedTextView : TextView {
|
|
|
214
241
|
val spannableString = SpannableString(currentText)
|
|
215
242
|
|
|
216
243
|
wordPositions.forEach { wordPos ->
|
|
244
|
+
val wordColor = if (indicatorMode == "fill" &&
|
|
245
|
+
indicatorWordIndex >= 0 &&
|
|
246
|
+
wordPos.index <= indicatorWordIndex &&
|
|
247
|
+
indicatorColor.isNotEmpty()) {
|
|
248
|
+
parseColor(indicatorColor)
|
|
249
|
+
} else {
|
|
250
|
+
parseColor(textColor)
|
|
251
|
+
}
|
|
217
252
|
spannableString.setSpan(
|
|
218
253
|
WordClickableSpan(
|
|
219
254
|
wordIndex = wordPos.index,
|
|
220
255
|
word = wordPos.word,
|
|
221
|
-
wordColor =
|
|
256
|
+
wordColor = wordColor
|
|
222
257
|
),
|
|
223
258
|
wordPos.start,
|
|
224
259
|
wordPos.end,
|
|
@@ -244,6 +279,14 @@ class AdvancedTextView : TextView {
|
|
|
244
279
|
|
|
245
280
|
setLineSpacing(0f, lineHeightMultiplier)
|
|
246
281
|
|
|
282
|
+
// Android's letterSpacing is in em; the prop is in dp/points. The ratio
|
|
283
|
+
// dp / fontSize is scale-invariant, so no density conversion is needed.
|
|
284
|
+
letterSpacing = if (letterSpacingDp != 0f && fontSize > 0f) {
|
|
285
|
+
letterSpacingDp / fontSize
|
|
286
|
+
} else {
|
|
287
|
+
0f
|
|
288
|
+
}
|
|
289
|
+
|
|
247
290
|
|
|
248
291
|
post {
|
|
249
292
|
setText(spannableString, BufferType.SPANNABLE)
|
|
@@ -299,8 +342,8 @@ class AdvancedTextView : TextView {
|
|
|
299
342
|
}
|
|
300
343
|
}
|
|
301
344
|
|
|
302
|
-
// Draw indicator
|
|
303
|
-
if (indicatorWordIndex >= 0 && indicatorColor.isNotEmpty()) {
|
|
345
|
+
// Draw indicator background only in Highlight mode
|
|
346
|
+
if (indicatorMode != "fill" && indicatorWordIndex >= 0 && indicatorColor.isNotEmpty()) {
|
|
304
347
|
val wordPos = wordPositions.find { it.index == indicatorWordIndex }
|
|
305
348
|
if (wordPos != null) {
|
|
306
349
|
val start = wordPos.start
|
|
@@ -136,6 +136,14 @@ class AdvancedTextViewManager : SimpleViewManager<AdvancedTextView>() {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
@ReactProp(name = "indicatorMode")
|
|
140
|
+
fun setIndicatorMode(view: AdvancedTextView?, indicatorMode: String?) {
|
|
141
|
+
android.util.Log.d(NAME, "setIndicatorMode called with: $indicatorMode")
|
|
142
|
+
if (indicatorMode != null) {
|
|
143
|
+
view?.setIndicatorMode(indicatorMode)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
139
147
|
@ReactProp(name = "lineHeight")
|
|
140
148
|
fun setLineHeight(view: AdvancedTextView?, lineHeight: Float) {
|
|
141
149
|
android.util.Log.d(NAME, "setLineHeight called with: $lineHeight")
|
|
@@ -144,6 +152,12 @@ class AdvancedTextViewManager : SimpleViewManager<AdvancedTextView>() {
|
|
|
144
152
|
}
|
|
145
153
|
}
|
|
146
154
|
|
|
155
|
+
@ReactProp(name = "letterSpacing")
|
|
156
|
+
fun setLetterSpacing(view: AdvancedTextView?, letterSpacing: Float) {
|
|
157
|
+
android.util.Log.d(NAME, "setLetterSpacing called with: $letterSpacing")
|
|
158
|
+
view?.setAdvancedLetterSpacing(letterSpacing)
|
|
159
|
+
}
|
|
160
|
+
|
|
147
161
|
override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any> {
|
|
148
162
|
return mapOf(
|
|
149
163
|
"onWordPress" to mapOf("registrationName" to "onWordPress"),
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Implementation of the self-measuring <AdvancedTextView> ShadowNode.
|
|
3
|
+
* See ShadowNodes.h for why this file shadows the codegen output.
|
|
4
|
+
*
|
|
5
|
+
* NOTE: this file deliberately does NOT define `AdvancedTextViewComponentName`.
|
|
6
|
+
* That symbol stays owned by the codegen-generated `ShadowNodes.cpp`, which is
|
|
7
|
+
* still compiled (via the podspec on iOS and via CMakeLists.txt on Android).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
#include <react/renderer/components/AdvancedTextViewSpec/ShadowNodes.h>
|
|
11
|
+
|
|
12
|
+
#include <algorithm>
|
|
13
|
+
#include <cctype>
|
|
14
|
+
#include <string>
|
|
15
|
+
|
|
16
|
+
#include <react/renderer/attributedstring/AttributedStringBox.h>
|
|
17
|
+
#include <react/renderer/attributedstring/ParagraphAttributes.h>
|
|
18
|
+
#include <react/renderer/attributedstring/TextAttributes.h>
|
|
19
|
+
#include <react/renderer/attributedstring/primitives.h>
|
|
20
|
+
#include <react/renderer/mounting/ShadowView.h>
|
|
21
|
+
#include <react/renderer/textlayoutmanager/TextLayoutContext.h>
|
|
22
|
+
#include <react/renderer/textlayoutmanager/TextLayoutManager.h>
|
|
23
|
+
|
|
24
|
+
namespace facebook::react {
|
|
25
|
+
|
|
26
|
+
namespace {
|
|
27
|
+
|
|
28
|
+
std::string toLowerAscii(std::string value) {
|
|
29
|
+
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
|
|
30
|
+
return static_cast<char>(std::tolower(c));
|
|
31
|
+
});
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
} // namespace
|
|
36
|
+
|
|
37
|
+
AdvancedTextViewShadowNode::AdvancedTextViewShadowNode(
|
|
38
|
+
const ShadowNode& sourceShadowNode,
|
|
39
|
+
const ShadowNodeFragment& fragment)
|
|
40
|
+
: ConcreteViewShadowNode(sourceShadowNode, fragment),
|
|
41
|
+
textLayoutManager_(
|
|
42
|
+
static_cast<const AdvancedTextViewShadowNode&>(sourceShadowNode)
|
|
43
|
+
.textLayoutManager_) {}
|
|
44
|
+
|
|
45
|
+
AttributedString AdvancedTextViewShadowNode::getAttributedString() const {
|
|
46
|
+
const auto& props = getConcreteProps();
|
|
47
|
+
|
|
48
|
+
auto textAttributes = TextAttributes::defaultTextAttributes();
|
|
49
|
+
|
|
50
|
+
// Font size: matches the 16pt fallback used by the native views.
|
|
51
|
+
textAttributes.fontSize =
|
|
52
|
+
props.fontSize > 0 ? static_cast<Float>(props.fontSize) : 16.0;
|
|
53
|
+
|
|
54
|
+
if (!props.fontFamily.empty()) {
|
|
55
|
+
textAttributes.fontFamily = props.fontFamily;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The native views treat `fontWeight` as either a weight ("bold") or a
|
|
59
|
+
// style ("italic"); mirror that here so wrapping metrics line up.
|
|
60
|
+
const auto weight = toLowerAscii(props.fontWeight);
|
|
61
|
+
if (weight == "bold") {
|
|
62
|
+
textAttributes.fontWeight = FontWeight::Bold;
|
|
63
|
+
} else if (weight == "italic") {
|
|
64
|
+
textAttributes.fontStyle = FontStyle::Italic;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (props.letterSpacing != 0) {
|
|
68
|
+
textAttributes.letterSpacing = props.letterSpacing;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (props.lineHeight > 0) {
|
|
72
|
+
// The component treats `lineHeight` as a multiple of the font size
|
|
73
|
+
// (see AdvancedTextView.mm / AdvancedTextView.kt). `TextAttributes`
|
|
74
|
+
// expects an absolute value; approximate the platform default single
|
|
75
|
+
// line height as 1.2 * fontSize.
|
|
76
|
+
textAttributes.lineHeight =
|
|
77
|
+
props.lineHeight * textAttributes.fontSize * 1.2;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
auto attributedString = AttributedString{};
|
|
81
|
+
attributedString.appendFragment(AttributedString::Fragment{
|
|
82
|
+
.string = props.text,
|
|
83
|
+
.textAttributes = textAttributes,
|
|
84
|
+
.parentShadowView = ShadowView(*this),
|
|
85
|
+
});
|
|
86
|
+
return attributedString;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
Size AdvancedTextViewShadowNode::measureContent(
|
|
90
|
+
const LayoutContext& layoutContext,
|
|
91
|
+
const LayoutConstraints& layoutConstraints) const {
|
|
92
|
+
const auto& props = getConcreteProps();
|
|
93
|
+
|
|
94
|
+
if (props.text.empty()) {
|
|
95
|
+
return layoutConstraints.clamp({0, 0});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!textLayoutManager_) {
|
|
99
|
+
textLayoutManager_ =
|
|
100
|
+
std::make_shared<const TextLayoutManager>(getContextContainer());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
auto paragraphAttributes = ParagraphAttributes{};
|
|
104
|
+
paragraphAttributes.maximumNumberOfLines = 0; // unlimited -> natural wrapping
|
|
105
|
+
paragraphAttributes.adjustsFontSizeToFit = false;
|
|
106
|
+
|
|
107
|
+
auto textLayoutContext = TextLayoutContext{};
|
|
108
|
+
textLayoutContext.pointScaleFactor = layoutContext.pointScaleFactor;
|
|
109
|
+
textLayoutContext.surfaceId = getSurfaceId();
|
|
110
|
+
|
|
111
|
+
auto measurement = textLayoutManager_->measure(
|
|
112
|
+
AttributedStringBox{getAttributedString()},
|
|
113
|
+
paragraphAttributes,
|
|
114
|
+
textLayoutContext,
|
|
115
|
+
layoutConstraints);
|
|
116
|
+
|
|
117
|
+
return layoutConstraints.clamp(measurement.size);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
} // namespace facebook::react
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Custom, self-measuring ShadowNode for <AdvancedTextView>.
|
|
3
|
+
*
|
|
4
|
+
* This file intentionally *shadows* the file generated by react-native-codegen
|
|
5
|
+
* at:
|
|
6
|
+
* build/generated/source/codegen/jni/react/renderer/components/AdvancedTextViewSpec/ShadowNodes.h (Android)
|
|
7
|
+
* <codegen>/react/renderer/components/AdvancedTextViewSpec/ShadowNodes.h (iOS)
|
|
8
|
+
*
|
|
9
|
+
* The generated version defines `AdvancedTextViewShadowNode` as a plain
|
|
10
|
+
* `ConcreteViewShadowNode` alias, which has no Yoga measure function. That is
|
|
11
|
+
* why <AdvancedText> renders nothing unless an explicit `height` is provided.
|
|
12
|
+
*
|
|
13
|
+
* This version replaces the alias with a real class that opts into Yoga
|
|
14
|
+
* measurement (`LeafYogaNode | MeasurableYogaNode`) and implements
|
|
15
|
+
* `measureContent()` using React Native's own `TextLayoutManager` -- the exact
|
|
16
|
+
* engine <Text> uses -- so the component derives its height from its text and
|
|
17
|
+
* the available width, just like <Text>.
|
|
18
|
+
*
|
|
19
|
+
* The generated `ComponentDescriptors.h` (`using AdvancedTextViewComponentDescriptor
|
|
20
|
+
* = ConcreteComponentDescriptor<AdvancedTextViewShadowNode>`) therefore wraps
|
|
21
|
+
* this class automatically; no other wiring change is required on either
|
|
22
|
+
* platform. The `AdvancedTextViewComponentName` symbol is still defined by the
|
|
23
|
+
* generated `ShadowNodes.cpp`.
|
|
24
|
+
*
|
|
25
|
+
* For this shadowing to take effect, `common/cpp` must appear on the include
|
|
26
|
+
* path *before* the codegen output directory:
|
|
27
|
+
* - Android: handled by `android/CMakeLists.txt` (see `target_include_directories`)
|
|
28
|
+
* - iOS: handled by `AdvancedText.podspec` (HEADER_SEARCH_PATHS)
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
#pragma once
|
|
32
|
+
|
|
33
|
+
#include <memory>
|
|
34
|
+
|
|
35
|
+
#include <jsi/jsi.h>
|
|
36
|
+
#include <react/renderer/attributedstring/AttributedString.h>
|
|
37
|
+
#include <react/renderer/components/AdvancedTextViewSpec/EventEmitters.h>
|
|
38
|
+
#include <react/renderer/components/AdvancedTextViewSpec/Props.h>
|
|
39
|
+
#include <react/renderer/components/AdvancedTextViewSpec/States.h>
|
|
40
|
+
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
|
|
41
|
+
#include <react/renderer/core/LayoutConstraints.h>
|
|
42
|
+
#include <react/renderer/core/LayoutContext.h>
|
|
43
|
+
#include <react/renderer/core/ShadowNode.h>
|
|
44
|
+
#include <react/renderer/core/ShadowNodeFragment.h>
|
|
45
|
+
|
|
46
|
+
namespace facebook::react {
|
|
47
|
+
|
|
48
|
+
class TextLayoutManager;
|
|
49
|
+
|
|
50
|
+
// Defined by the codegen-generated ShadowNodes.cpp.
|
|
51
|
+
JSI_EXPORT extern const char AdvancedTextViewComponentName[];
|
|
52
|
+
|
|
53
|
+
/*
|
|
54
|
+
* `ShadowNode` for <AdvancedTextView> component.
|
|
55
|
+
*/
|
|
56
|
+
class AdvancedTextViewShadowNode final : public ConcreteViewShadowNode<
|
|
57
|
+
AdvancedTextViewComponentName,
|
|
58
|
+
AdvancedTextViewProps,
|
|
59
|
+
AdvancedTextViewEventEmitter,
|
|
60
|
+
AdvancedTextViewState> {
|
|
61
|
+
public:
|
|
62
|
+
// "create" constructor: (fragment, family, traits)
|
|
63
|
+
using ConcreteViewShadowNode::ConcreteViewShadowNode;
|
|
64
|
+
|
|
65
|
+
// "clone" constructor: carry the resolved TextLayoutManager across clones.
|
|
66
|
+
AdvancedTextViewShadowNode(
|
|
67
|
+
const ShadowNode& sourceShadowNode,
|
|
68
|
+
const ShadowNodeFragment& fragment);
|
|
69
|
+
|
|
70
|
+
static ShadowNodeTraits BaseTraits() {
|
|
71
|
+
auto traits = ConcreteViewShadowNode::BaseTraits();
|
|
72
|
+
// Opt into Yoga measurement. `MeasurableYogaNode` requires `LeafYogaNode`.
|
|
73
|
+
traits.set(ShadowNodeTraits::Trait::LeafYogaNode);
|
|
74
|
+
traits.set(ShadowNodeTraits::Trait::MeasurableYogaNode);
|
|
75
|
+
return traits;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#pragma mark - LayoutableShadowNode
|
|
79
|
+
|
|
80
|
+
Size measureContent(
|
|
81
|
+
const LayoutContext& layoutContext,
|
|
82
|
+
const LayoutConstraints& layoutConstraints) const override;
|
|
83
|
+
|
|
84
|
+
private:
|
|
85
|
+
/*
|
|
86
|
+
* Builds a single-fragment `AttributedString` from the component props
|
|
87
|
+
* (text + font attributes), mirroring how the native views render it.
|
|
88
|
+
*/
|
|
89
|
+
AttributedString getAttributedString() const;
|
|
90
|
+
|
|
91
|
+
// Lazily created on first measure, then reused / copied across clones.
|
|
92
|
+
mutable std::shared_ptr<const TextLayoutManager> textLayoutManager_;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
} // namespace facebook::react
|
package/ios/AdvancedTextView.mm
CHANGED
|
@@ -23,8 +23,10 @@ using namespace facebook::react;
|
|
|
23
23
|
@property (nonatomic, strong) NSString *textAlign;
|
|
24
24
|
@property (nonatomic, strong) NSString *fontFamily;
|
|
25
25
|
@property (nonatomic, assign) CGFloat lineHeight;
|
|
26
|
+
@property (nonatomic, assign) CGFloat letterSpacing;
|
|
26
27
|
@property (nonatomic, assign) NSInteger indicatorWordIndex;
|
|
27
28
|
@property (nonatomic, strong) UIColor *indicatorColor;
|
|
29
|
+
@property (nonatomic, strong) NSString *indicatorMode;
|
|
28
30
|
|
|
29
31
|
- (void)handleCustomMenuAction:(UIMenuItem *)sender;
|
|
30
32
|
- (void)drawHighlightedBackgroundsInTextView:(UITextView *)textView;
|
|
@@ -98,8 +100,10 @@ using namespace facebook::react;
|
|
|
98
100
|
_textAlign = @"left";
|
|
99
101
|
_fontFamily = @"System";
|
|
100
102
|
_lineHeight = 0.0;
|
|
103
|
+
_letterSpacing = 0.0;
|
|
101
104
|
_indicatorWordIndex = -1;
|
|
102
105
|
_indicatorColor = nil;
|
|
106
|
+
_indicatorMode = @"Highlight";
|
|
103
107
|
|
|
104
108
|
[self setupTextView];
|
|
105
109
|
[self setupGestureRecognizers];
|
|
@@ -122,9 +126,17 @@ using namespace facebook::react;
|
|
|
122
126
|
_textView.parentView = self;
|
|
123
127
|
_textView.editable = NO;
|
|
124
128
|
_textView.selectable = YES;
|
|
125
|
-
|
|
129
|
+
// The Fabric ShadowNode now measures the intrinsic content height, so the
|
|
130
|
+
// native view is always laid out tall enough to fit its text. Disable
|
|
131
|
+
// scrolling so the component behaves like <Text> (content grows / clips
|
|
132
|
+
// rather than scrolls).
|
|
133
|
+
_textView.scrollEnabled = NO;
|
|
126
134
|
_textView.backgroundColor = [UIColor clearColor];
|
|
127
|
-
|
|
135
|
+
// Zero all intrinsic padding so the rendered text box matches the size
|
|
136
|
+
// reported by the ShadowNode's measureContent(). Consumers add padding
|
|
137
|
+
// via React Native `style` instead, exactly like <Text>.
|
|
138
|
+
_textView.textContainerInset = UIEdgeInsetsZero;
|
|
139
|
+
_textView.textContainer.lineFragmentPadding = 0;
|
|
128
140
|
_textView.font = [UIFont systemFontOfSize:16];
|
|
129
141
|
_textView.textColor = [UIColor labelColor];
|
|
130
142
|
_textView.delegate = self;
|
|
@@ -234,10 +246,26 @@ using namespace facebook::react;
|
|
|
234
246
|
styleChanged = YES;
|
|
235
247
|
}
|
|
236
248
|
|
|
249
|
+
if (oldViewProps.letterSpacing != newViewProps.letterSpacing) {
|
|
250
|
+
NSLog(@"[AdvancedTextView] Updating letterSpacing to: %f", newViewProps.letterSpacing);
|
|
251
|
+
_letterSpacing = static_cast<CGFloat>(newViewProps.letterSpacing);
|
|
252
|
+
styleChanged = YES;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
BOOL indicatorChanged = NO;
|
|
256
|
+
|
|
257
|
+
if (oldViewProps.indicatorMode != newViewProps.indicatorMode) {
|
|
258
|
+
NSLog(@"[AdvancedTextView] Updating indicatorMode to: %s", newViewProps.indicatorMode.c_str());
|
|
259
|
+
_indicatorMode = newViewProps.indicatorMode.empty()
|
|
260
|
+
? @"Highlight"
|
|
261
|
+
: [NSString stringWithUTF8String:newViewProps.indicatorMode.c_str()];
|
|
262
|
+
indicatorChanged = YES;
|
|
263
|
+
}
|
|
264
|
+
|
|
237
265
|
if (oldViewProps.indicatorWordIndex != newViewProps.indicatorWordIndex) {
|
|
238
266
|
NSLog(@"[AdvancedTextView] Updating indicatorWordIndex to: %d", newViewProps.indicatorWordIndex);
|
|
239
267
|
_indicatorWordIndex = static_cast<NSInteger>(newViewProps.indicatorWordIndex);
|
|
240
|
-
|
|
268
|
+
indicatorChanged = YES;
|
|
241
269
|
}
|
|
242
270
|
|
|
243
271
|
if (oldViewProps.indicatorColor != newViewProps.indicatorColor) {
|
|
@@ -248,7 +276,15 @@ using namespace facebook::react;
|
|
|
248
276
|
} else {
|
|
249
277
|
_indicatorColor = nil;
|
|
250
278
|
}
|
|
251
|
-
|
|
279
|
+
indicatorChanged = YES;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (indicatorChanged) {
|
|
283
|
+
if ([_indicatorMode isEqualToString:@"fill"]) {
|
|
284
|
+
[self updateTextAppearance]; // rebuilds attributed text with fill colors; calls setNeedsDisplay
|
|
285
|
+
} else {
|
|
286
|
+
[_textView setNeedsDisplay];
|
|
287
|
+
}
|
|
252
288
|
}
|
|
253
289
|
|
|
254
290
|
if (textChanged) {
|
|
@@ -446,12 +482,30 @@ using namespace facebook::react;
|
|
|
446
482
|
value:font
|
|
447
483
|
range:NSMakeRange(0, attributedString.length)];
|
|
448
484
|
|
|
485
|
+
if (_letterSpacing != 0.0) {
|
|
486
|
+
[attributedString addAttribute:NSKernAttributeName
|
|
487
|
+
value:@(_letterSpacing)
|
|
488
|
+
range:NSMakeRange(0, attributedString.length)];
|
|
489
|
+
}
|
|
490
|
+
|
|
449
491
|
|
|
450
492
|
UIColor *color = _textColor ?: [UIColor labelColor];
|
|
451
493
|
[attributedString addAttribute:NSForegroundColorAttributeName
|
|
452
494
|
value:color
|
|
453
495
|
range:NSMakeRange(0, attributedString.length)];
|
|
454
496
|
|
|
497
|
+
if ([_indicatorMode isEqualToString:@"fill"] && _indicatorWordIndex >= 0 && _indicatorColor) {
|
|
498
|
+
for (NSDictionary *wordInfo in _wordRanges) {
|
|
499
|
+
NSInteger idx = [wordInfo[@"index"] integerValue];
|
|
500
|
+
if (idx <= _indicatorWordIndex) {
|
|
501
|
+
NSRange wordRange = [wordInfo[@"range"] rangeValue];
|
|
502
|
+
[attributedString addAttribute:NSForegroundColorAttributeName
|
|
503
|
+
value:_indicatorColor
|
|
504
|
+
range:wordRange];
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
455
509
|
_textView.attributedText = attributedString;
|
|
456
510
|
[_textView setNeedsDisplay];
|
|
457
511
|
|
|
@@ -513,7 +567,7 @@ using namespace facebook::react;
|
|
|
513
567
|
}];
|
|
514
568
|
}
|
|
515
569
|
|
|
516
|
-
if (_indicatorWordIndex >= 0 && _indicatorColor) {
|
|
570
|
+
if (![_indicatorMode isEqualToString:@"fill"] && _indicatorWordIndex >= 0 && _indicatorColor) {
|
|
517
571
|
for (NSDictionary *wordInfo in _wordRanges) {
|
|
518
572
|
NSNumber *index = wordInfo[@"index"];
|
|
519
573
|
if ([index integerValue] != _indicatorWordIndex) {
|
|
@@ -22,8 +22,10 @@ export interface NativeProps extends ViewProps {
|
|
|
22
22
|
textAlign?: string;
|
|
23
23
|
fontFamily?: string;
|
|
24
24
|
lineHeight?: Float;
|
|
25
|
+
letterSpacing?: Float;
|
|
25
26
|
indicatorWordIndex?: Int32;
|
|
26
27
|
indicatorColor?: string;
|
|
28
|
+
indicatorMode?: string;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export default codegenNativeComponent<NativeProps>('AdvancedTextView');
|
|
@@ -23,8 +23,10 @@ export interface NativeProps extends ViewProps {
|
|
|
23
23
|
textAlign?: string;
|
|
24
24
|
fontFamily?: string;
|
|
25
25
|
lineHeight?: Float;
|
|
26
|
+
letterSpacing?: Float;
|
|
26
27
|
indicatorWordIndex?: Int32;
|
|
27
28
|
indicatorColor?: string;
|
|
29
|
+
indicatorMode?: string;
|
|
28
30
|
}
|
|
29
31
|
declare const _default: import("react-native/types_generated/Libraries/Utilities/codegenNativeComponent").NativeComponentType<NativeProps>;
|
|
30
32
|
export default _default;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdvancedTextViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/AdvancedTextViewNativeComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAG,KAAK,EAAG,MAAM,2CAA2C,CAAC;AAEpG,UAAU,eAAe;IACvB,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAClD,WAAW,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,kBAAkB,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC;IACjE,WAAW,CAAC,EAAE,kBAAkB,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1E,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,KAAK,CAAC;IACnB,kBAAkB,CAAC,EAAE,KAAK,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"AdvancedTextViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/AdvancedTextViewNativeComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAG,KAAK,EAAG,MAAM,2CAA2C,CAAC;AAEpG,UAAU,eAAe;IACvB,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAClD,WAAW,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,kBAAkB,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC;IACjE,WAAW,CAAC,EAAE,kBAAkB,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1E,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,KAAK,CAAC;IACnB,aAAa,CAAC,EAAE,KAAK,CAAC;IACtB,kBAAkB,CAAC,EAAE,KAAK,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;;AAED,wBAAuE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-advanced-text",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": " Advanced text component for React Native with custom select options.",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"android",
|
|
19
19
|
"ios",
|
|
20
20
|
"cpp",
|
|
21
|
+
"common",
|
|
21
22
|
"*.podspec",
|
|
22
23
|
"react-native.config.js",
|
|
23
24
|
"!ios/build",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autolinking config for react-native-advanced-text.
|
|
3
|
+
*
|
|
4
|
+
* On Android we ship a custom C++ ShadowNode (see android/CMakeLists.txt and
|
|
5
|
+
* common/cpp/...) so the Fabric component measures its own height from its text
|
|
6
|
+
* and the available width. Pointing `cmakeListsPath` at our own CMakeLists.txt
|
|
7
|
+
* makes the app build that instead of the plain codegen output.
|
|
8
|
+
*
|
|
9
|
+
* `libraryName` (AdvancedTextViewSpec) and `componentDescriptors`
|
|
10
|
+
* (AdvancedTextViewComponentDescriptor) are still auto-derived by the CLI from
|
|
11
|
+
* package.json `codegenConfig` and the JS spec, so they are intentionally not
|
|
12
|
+
* listed here.
|
|
13
|
+
*/
|
|
14
|
+
module.exports = {
|
|
15
|
+
dependency: {
|
|
16
|
+
platforms: {
|
|
17
|
+
android: {
|
|
18
|
+
cmakeListsPath: 'CMakeLists.txt',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -22,8 +22,10 @@ export interface NativeProps extends ViewProps {
|
|
|
22
22
|
textAlign?: string;
|
|
23
23
|
fontFamily?: string;
|
|
24
24
|
lineHeight?: Float;
|
|
25
|
+
letterSpacing?: Float;
|
|
25
26
|
indicatorWordIndex?: Int32;
|
|
26
27
|
indicatorColor?: string;
|
|
28
|
+
indicatorMode?: string;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export default codegenNativeComponent<NativeProps>('AdvancedTextView');
|