react-native-advanced-text 0.1.44 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AdvancedText.podspec +42 -1
- package/README.md +32 -1
- package/android/CMakeLists.txt +51 -0
- package/android/src/main/java/com/advancedtext/AdvancedTextView.kt +20 -1
- package/android/src/main/java/com/advancedtext/AdvancedTextViewManager.kt +6 -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 +24 -2
- package/lib/module/AdvancedTextViewNativeComponent.ts +1 -0
- package/lib/typescript/src/AdvancedTextViewNativeComponent.d.ts +1 -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 +1 -0
package/AdvancedText.podspec
CHANGED
|
@@ -2,6 +2,16 @@ require "json"
|
|
|
2
2
|
|
|
3
3
|
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
4
|
|
|
5
|
+
# Resolve the react-native source dir so we can point at the Fabric text
|
|
6
|
+
# headers directly (they are not exposed by `install_modules_dependencies`).
|
|
7
|
+
react_common_dir =
|
|
8
|
+
begin
|
|
9
|
+
resolved = `cd "#{__dir__}" && node --print "require.resolve('react-native/package.json')" 2>/dev/null`.strip
|
|
10
|
+
(resolved.empty? || !File.exist?(resolved)) ? "" : File.join(File.dirname(resolved), "ReactCommon")
|
|
11
|
+
rescue StandardError
|
|
12
|
+
""
|
|
13
|
+
end
|
|
14
|
+
|
|
5
15
|
Pod::Spec.new do |s|
|
|
6
16
|
s.name = "AdvancedText"
|
|
7
17
|
s.version = package["version"]
|
|
@@ -13,8 +23,39 @@ Pod::Spec.new do |s|
|
|
|
13
23
|
s.platforms = { :ios => min_ios_version_supported }
|
|
14
24
|
s.source = { :git => "https://github.com/AminAllahham/react-native-advanced-text.git", :tag => "#{s.version}" }
|
|
15
25
|
|
|
16
|
-
|
|
26
|
+
# `common/cpp` holds the self-measuring Fabric ShadowNode. Its
|
|
27
|
+
# ShadowNodes.h intentionally shadows the codegen output, so `common/cpp`
|
|
28
|
+
# must come first on the header search path (see below).
|
|
29
|
+
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}", "common/cpp/**/*.{h,cpp}"
|
|
17
30
|
s.private_header_files = "ios/**/*.h"
|
|
18
31
|
|
|
19
32
|
install_modules_dependencies(s)
|
|
33
|
+
|
|
34
|
+
# `install_modules_dependencies` sets `s.pod_target_xcconfig`; extend it
|
|
35
|
+
# (rather than replace it) so:
|
|
36
|
+
# 1. our `common/cpp` headers win over the codegen output, and
|
|
37
|
+
# 2. <react/renderer/textlayoutmanager/*.h> resolves (TextLayoutManager /
|
|
38
|
+
# TextLayoutContext), which `install_modules_dependencies` does NOT add.
|
|
39
|
+
xcconfig = s.to_hash["pod_target_xcconfig"] || {}
|
|
40
|
+
|
|
41
|
+
# `common/cpp` MUST stay first so our ShadowNodes.h shadows the codegen one.
|
|
42
|
+
leading_paths = ["\"$(PODS_TARGET_SRCROOT)/common/cpp\""]
|
|
43
|
+
|
|
44
|
+
# Extra paths for the Fabric text headers (append order is fine here).
|
|
45
|
+
trailing_paths = [
|
|
46
|
+
"\"$(PODS_ROOT)/Headers/Public/React-FabricComponents\"",
|
|
47
|
+
"\"$(PODS_ROOT)/Headers/Private/React-FabricComponents\"",
|
|
48
|
+
]
|
|
49
|
+
unless react_common_dir.empty?
|
|
50
|
+
trailing_paths << "\"#{react_common_dir}\""
|
|
51
|
+
trailing_paths << "\"#{File.join(react_common_dir, "react/renderer/textlayoutmanager/platform/ios")}\""
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
existing_header_paths = xcconfig["HEADER_SEARCH_PATHS"]
|
|
55
|
+
xcconfig["HEADER_SEARCH_PATHS"] =
|
|
56
|
+
(leading_paths + [existing_header_paths].compact + trailing_paths).join(" ")
|
|
57
|
+
xcconfig["CLANG_CXX_LANGUAGE_STANDARD"] ||= "c++20"
|
|
58
|
+
s.pod_target_xcconfig = xcconfig
|
|
59
|
+
|
|
60
|
+
s.dependency "React-FabricComponents"
|
|
20
61
|
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,6 +41,8 @@ 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 = ""
|
|
@@ -56,7 +58,10 @@ class AdvancedTextView : TextView {
|
|
|
56
58
|
Log.d(TAG, "AdvancedTextView initialized")
|
|
57
59
|
|
|
58
60
|
textSize = 16f
|
|
59
|
-
|
|
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)
|
|
60
65
|
setTextIsSelectable(true)
|
|
61
66
|
|
|
62
67
|
movementMethod = SmartMovementMethod
|
|
@@ -148,6 +153,12 @@ class AdvancedTextView : TextView {
|
|
|
148
153
|
updateTextWithHighlights()
|
|
149
154
|
}
|
|
150
155
|
|
|
156
|
+
fun setAdvancedLetterSpacing(spacingDp: Float) {
|
|
157
|
+
if (letterSpacingDp == spacingDp) return
|
|
158
|
+
letterSpacingDp = spacingDp
|
|
159
|
+
updateTextWithHighlights()
|
|
160
|
+
}
|
|
161
|
+
|
|
151
162
|
|
|
152
163
|
fun setAdvancedFontFamily(family: String) {
|
|
153
164
|
if (fontFamily == family) return
|
|
@@ -268,6 +279,14 @@ class AdvancedTextView : TextView {
|
|
|
268
279
|
|
|
269
280
|
setLineSpacing(0f, lineHeightMultiplier)
|
|
270
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
|
+
|
|
271
290
|
|
|
272
291
|
post {
|
|
273
292
|
setText(spannableString, BufferType.SPANNABLE)
|
|
@@ -152,6 +152,12 @@ class AdvancedTextViewManager : SimpleViewManager<AdvancedTextView>() {
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
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
|
+
|
|
155
161
|
override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any> {
|
|
156
162
|
return mapOf(
|
|
157
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,6 +23,7 @@ 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;
|
|
28
29
|
@property (nonatomic, strong) NSString *indicatorMode;
|
|
@@ -99,6 +100,7 @@ using namespace facebook::react;
|
|
|
99
100
|
_textAlign = @"left";
|
|
100
101
|
_fontFamily = @"System";
|
|
101
102
|
_lineHeight = 0.0;
|
|
103
|
+
_letterSpacing = 0.0;
|
|
102
104
|
_indicatorWordIndex = -1;
|
|
103
105
|
_indicatorColor = nil;
|
|
104
106
|
_indicatorMode = @"Highlight";
|
|
@@ -124,9 +126,17 @@ using namespace facebook::react;
|
|
|
124
126
|
_textView.parentView = self;
|
|
125
127
|
_textView.editable = NO;
|
|
126
128
|
_textView.selectable = YES;
|
|
127
|
-
|
|
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;
|
|
128
134
|
_textView.backgroundColor = [UIColor clearColor];
|
|
129
|
-
|
|
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;
|
|
130
140
|
_textView.font = [UIFont systemFontOfSize:16];
|
|
131
141
|
_textView.textColor = [UIColor labelColor];
|
|
132
142
|
_textView.delegate = self;
|
|
@@ -236,6 +246,12 @@ using namespace facebook::react;
|
|
|
236
246
|
styleChanged = YES;
|
|
237
247
|
}
|
|
238
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
|
+
|
|
239
255
|
BOOL indicatorChanged = NO;
|
|
240
256
|
|
|
241
257
|
if (oldViewProps.indicatorMode != newViewProps.indicatorMode) {
|
|
@@ -466,6 +482,12 @@ using namespace facebook::react;
|
|
|
466
482
|
value:font
|
|
467
483
|
range:NSMakeRange(0, attributedString.length)];
|
|
468
484
|
|
|
485
|
+
if (_letterSpacing != 0.0) {
|
|
486
|
+
[attributedString addAttribute:NSKernAttributeName
|
|
487
|
+
value:@(_letterSpacing)
|
|
488
|
+
range:NSMakeRange(0, attributedString.length)];
|
|
489
|
+
}
|
|
490
|
+
|
|
469
491
|
|
|
470
492
|
UIColor *color = _textColor ?: [UIColor labelColor];
|
|
471
493
|
[attributedString addAttribute:NSForegroundColorAttributeName
|
|
@@ -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;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;;AAED,wBAAuE"}
|
|
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.1
|
|
3
|
+
"version": "0.2.1",
|
|
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
|
+
};
|